mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
64 Commits
0.4.5
...
core_repor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae486ad04 | ||
|
|
25a5e37cc6 | ||
|
|
860df6e658 | ||
|
|
16525c91b8 | ||
|
|
30cdde8605 | ||
|
|
38c54ba393 | ||
|
|
5da3a40964 | ||
|
|
a1ef25455b | ||
|
|
feb62b4af2 | ||
|
|
6dbeb6c658 | ||
|
|
09abc5beaa | ||
|
|
fb1cf26118 | ||
|
|
842a15fd3c | ||
|
|
f67e8497e0 | ||
|
|
19f1b265b1 | ||
|
|
1c14d1107f | ||
|
|
7b3727e945 | ||
|
|
d1274c12b9 | ||
|
|
7c44d749d4 | ||
|
|
f1765899c4 | ||
|
|
87b9f4a6fa | ||
|
|
480c15eea0 | ||
|
|
d4ec4d32f0 | ||
|
|
50fb70d832 | ||
|
|
ca5c0d9eb8 | ||
|
|
98e2d89a1c | ||
|
|
5bda265f43 | ||
|
|
9ef1fa825d | ||
|
|
f5ff005360 | ||
|
|
fb3035a78b | ||
|
|
3d5c21cec5 | ||
|
|
feaae1db12 | ||
|
|
56ec0e902d | ||
|
|
c8fd42f938 | ||
|
|
e5eb54fb27 | ||
|
|
fbeba1e470 | ||
|
|
11073dfdac | ||
|
|
7e660dbd23 | ||
|
|
1fc2c5ed4b | ||
|
|
242126b4b2 | ||
|
|
30dda013de | ||
|
|
ea4d0abb7f | ||
|
|
9bbc49204d | ||
|
|
b97cee4372 | ||
|
|
5dcafac861 | ||
|
|
8d28c34f95 | ||
|
|
0f2bd0fb85 | ||
|
|
cf59cfaad0 | ||
|
|
c534cb79b5 | ||
|
|
8952fbdc03 | ||
|
|
9012afeb75 | ||
|
|
401a6d9a42 | ||
|
|
5d4ed00452 | ||
|
|
0ba6d90bb8 | ||
|
|
b007a42487 | ||
|
|
32c92e03e7 | ||
|
|
14437d653f | ||
|
|
1021df8b1b | ||
|
|
569c37cb8e | ||
|
|
b296f64d3c | ||
|
|
8d9bd0135e | ||
|
|
885fb4aabe | ||
|
|
3c564d7b96 | ||
|
|
5e5792009c |
27
.github/pyright-config.json
vendored
Normal file
27
.github/pyright-config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"include": [
|
||||
"type_check.py",
|
||||
"../worlds/AutoSNIClient.py",
|
||||
"../Patch.py"
|
||||
],
|
||||
|
||||
"exclude": [
|
||||
"**/__pycache__"
|
||||
],
|
||||
|
||||
"stubPath": "../typings",
|
||||
|
||||
"typeCheckingMode": "strict",
|
||||
"reportImplicitOverride": "error",
|
||||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": true,
|
||||
|
||||
"pythonVersion": "3.8",
|
||||
"pythonPlatform": "Windows",
|
||||
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.github/type_check.py
vendored
Normal file
15
.github/type_check.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
config = Path(__file__).parent / "pyright-config.json"
|
||||
|
||||
command = ("pyright", "-p", str(config))
|
||||
print(" ".join(command))
|
||||
|
||||
try:
|
||||
result = subprocess.run(command)
|
||||
except FileNotFoundError as e:
|
||||
print(f"{e} - Is pyright installed?")
|
||||
exit(1)
|
||||
|
||||
exit(result.returncode)
|
||||
33
.github/workflows/strict-type-check.yml
vendored
Normal file
33
.github/workflows/strict-type-check.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: type check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.py"
|
||||
- ".github/pyright-config.json"
|
||||
- ".github/workflows/strict-type-check.yml"
|
||||
- "**.pyi"
|
||||
push:
|
||||
paths:
|
||||
- "**.py"
|
||||
- ".github/pyright-config.json"
|
||||
- ".github/workflows/strict-type-check.yml"
|
||||
- "**.pyi"
|
||||
|
||||
jobs:
|
||||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
python -m pip install --upgrade pip pyright==1.1.358
|
||||
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
|
||||
|
||||
- name: "pyright: strict check on specific files"
|
||||
run: python .github/type_check.py
|
||||
@@ -7,6 +7,8 @@ import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
import threading
|
||||
import time
|
||||
from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
@@ -95,6 +97,42 @@ class MultiWorld():
|
||||
def __getitem__(self, player) -> bool:
|
||||
return self.rule(player)
|
||||
|
||||
class Observer(threading.Thread):
|
||||
current_function: str
|
||||
entered: float
|
||||
shutdown: bool = False
|
||||
|
||||
def __init__(self):
|
||||
self.current_function = ""
|
||||
self.entered = 0.0
|
||||
super().__init__(name="Observer", daemon=True)
|
||||
|
||||
def __call__(self, function: typing.Callable, entered: float):
|
||||
# use str of function to avoid having a reference to a bound method
|
||||
self.current_function = str(function)
|
||||
self.entered = entered
|
||||
return self
|
||||
|
||||
def __enter__(self):
|
||||
assert self.current_function, "Entered Observer Context without current method."
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.current_function = ""
|
||||
|
||||
def run(self):
|
||||
while not self.shutdown:
|
||||
time.sleep(1)
|
||||
if self.current_function:
|
||||
now = time.perf_counter()
|
||||
elapsed = now - self.entered
|
||||
if elapsed > 60:
|
||||
logging.info(f"Generation stalling in {self.current_function}, "
|
||||
f"running since {elapsed:.0f} seconds ago.")
|
||||
self.current_function = ""
|
||||
|
||||
observer = Observer()
|
||||
observer.start()
|
||||
|
||||
class RegionManager:
|
||||
region_cache: Dict[int, Dict[str, Region]]
|
||||
entrance_cache: Dict[int, Dict[str, Entrance]]
|
||||
@@ -160,14 +198,6 @@ class MultiWorld():
|
||||
self.local_early_items = {player: {} for player in self.player_ids}
|
||||
self.indirect_connections = {}
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
self.fix_trock_doors = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_palaceofdarkness_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
self.fix_trock_exit = self.AttributeProxy(
|
||||
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
@@ -445,7 +475,7 @@ class MultiWorld():
|
||||
location.item = item
|
||||
item.location = location
|
||||
if collect:
|
||||
self.state.collect(item, location.event, location)
|
||||
self.state.collect(item, location.advancement, location)
|
||||
|
||||
logging.debug('Placed %s at %s', item, location)
|
||||
|
||||
@@ -592,8 +622,7 @@ class MultiWorld():
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
if location.progress_type != LocationProgressType.EXCLUDED \
|
||||
and (location.player in players["locations"] or location.event
|
||||
or (location.item and location.item.advancement)):
|
||||
and (location.player in players["locations"] or location.advancement):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -738,7 +767,7 @@ class CollectionState():
|
||||
locations = self.multiworld.get_filled_locations()
|
||||
reachable_events = True
|
||||
# since the loop has a good chance to run more than once, only filter the events once
|
||||
locations = {location for location in locations if location.event and location not in self.events and
|
||||
locations = {location for location in locations if location.advancement and location not in self.events and
|
||||
not key_only or getattr(location.item, "locked_dungeon_item", False)}
|
||||
while reachable_events:
|
||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||
@@ -1028,7 +1057,6 @@ class Location:
|
||||
name: str
|
||||
address: Optional[int]
|
||||
parent_region: Optional[Region]
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
@@ -1059,7 +1087,6 @@ class Location:
|
||||
raise Exception(f"Location {self} already filled.")
|
||||
self.item = item
|
||||
item.location = self
|
||||
self.event = item.advancement
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
@@ -1075,6 +1102,15 @@ class Location:
|
||||
def __lt__(self, other: Location):
|
||||
return (self.player, self.name) < (other.player, other.name)
|
||||
|
||||
@property
|
||||
def advancement(self) -> bool:
|
||||
return self.item is not None and self.item.advancement
|
||||
|
||||
@property
|
||||
def is_event(self) -> bool:
|
||||
"""Returns True if the address of this location is None, denoting it is an Event Location."""
|
||||
return self.address is None
|
||||
|
||||
@property
|
||||
def native_item(self) -> bool:
|
||||
"""Returns True if the item in this location matches game."""
|
||||
@@ -1352,12 +1388,15 @@ class Spoiler:
|
||||
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
def to_file(self, filename: str) -> None:
|
||||
from itertools import chain
|
||||
from worlds import AutoWorld
|
||||
from Options import Visibility
|
||||
|
||||
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
|
||||
res = getattr(self.multiworld.worlds[player].options, option_key)
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
|
||||
if res.visibility & Visibility.spoiler:
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
|
||||
|
||||
with open(filename, 'w', encoding="utf-8-sig") as outfile:
|
||||
outfile.write(
|
||||
@@ -1388,6 +1427,14 @@ class Spoiler:
|
||||
|
||||
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
||||
|
||||
precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})"
|
||||
if self.multiworld.players > 1
|
||||
else item.name
|
||||
for item in chain.from_iterable(self.multiworld.precollected_items.values())]
|
||||
if precollected_items:
|
||||
outfile.write("\n\nStarting Items:\n\n")
|
||||
outfile.write("\n".join([item for item in precollected_items]))
|
||||
|
||||
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
|
||||
for location in self.multiworld.get_locations() if location.show_in_spoiler]
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
|
||||
@@ -193,6 +193,7 @@ class CommonContext:
|
||||
server_version: Version = Version(0, 0, 0)
|
||||
generator_version: Version = Version(0, 0, 0)
|
||||
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
|
||||
max_size: int = 16*1024*1024 # 16 MB of max incoming packet size
|
||||
|
||||
last_death_link: float = time.time() # last send/received death link on AP layer
|
||||
|
||||
@@ -651,7 +652,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
try:
|
||||
port = server_url.port or 38281 # raises ValueError if invalid
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None,
|
||||
max_size=ctx.max_size)
|
||||
if ctx.ui is not None:
|
||||
ctx.ui.update_address_bar(server_url.netloc)
|
||||
ctx.server = Endpoint(socket)
|
||||
|
||||
10
Fill.py
10
Fill.py
@@ -159,7 +159,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
_log_fill_progress(name, placed, total)
|
||||
@@ -310,7 +309,6 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
location.event = False
|
||||
if location in state.events:
|
||||
state.events.remove(location)
|
||||
locations.append(location)
|
||||
@@ -659,7 +657,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
while True:
|
||||
# Check locations in the current sphere and gather progression items to swap earlier
|
||||
for location in balancing_sphere:
|
||||
if location.event:
|
||||
if location.advancement:
|
||||
balancing_state.collect(location.item, True, location)
|
||||
player = location.item.player
|
||||
# only replace items that end up in another player's world
|
||||
@@ -716,7 +714,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
|
||||
replacement_locations = sorted(l for l in checked_locations if not l.advancement and not l.locked)
|
||||
multiworld.random.shuffle(replacement_locations)
|
||||
items_to_replace.sort()
|
||||
multiworld.random.shuffle(items_to_replace)
|
||||
@@ -747,7 +745,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
sphere_locations.add(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event:
|
||||
if location.advancement:
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
@@ -768,7 +766,6 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
||||
location_2.item, location_1.item = location_1.item, location_2.item
|
||||
location_1.item.location = location_1
|
||||
location_2.item.location = location_2
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
@@ -965,7 +962,6 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
|
||||
18
Generate.py
18
Generate.py
@@ -21,7 +21,6 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Main import main as ERmain
|
||||
from settings import get_settings
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
from worlds.alttp import Options as LttPOptions
|
||||
from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
@@ -35,8 +34,8 @@ def mystery_argparse():
|
||||
|
||||
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
|
||||
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
|
||||
help='Path to the weights file to use for rolling game settings, urls are also valid')
|
||||
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
||||
help='Path to the weights file to use for rolling game options, urls are also valid')
|
||||
parser.add_argument('--sameoptions', help='Rolls options per weights file rather than per player',
|
||||
action='store_true')
|
||||
parser.add_argument('--player_files_path', default=defaults.player_files_path,
|
||||
help="Input directory for player files.")
|
||||
@@ -104,8 +103,8 @@ def main(args=None, callback=ERmain):
|
||||
del(meta_weights["meta_description"])
|
||||
except Exception as e:
|
||||
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
|
||||
if args.samesettings:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
if args.sameoptions:
|
||||
raise Exception("Cannot mix --sameoptions with --meta")
|
||||
else:
|
||||
meta_weights = None
|
||||
player_id = 1
|
||||
@@ -157,7 +156,7 @@ def main(args=None, callback=ERmain):
|
||||
erargs.skip_output = args.skip_output
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
@@ -311,13 +310,6 @@ def handle_name(name: str, player: int, name_counter: Counter):
|
||||
return new_name
|
||||
|
||||
|
||||
def prefer_int(input_data: str) -> Union[str, int]:
|
||||
try:
|
||||
return int(input_data)
|
||||
except:
|
||||
return input_data
|
||||
|
||||
|
||||
def roll_percentage(percentage: Union[int, float]) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
|
||||
@@ -70,7 +70,7 @@ def install_pkg_resources(yes=False):
|
||||
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
|
||||
|
||||
|
||||
def update(yes=False, force=False):
|
||||
def update(yes: bool = False, force: bool = False) -> None:
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
|
||||
@@ -586,7 +586,7 @@ class Context:
|
||||
self.location_check_points = savedata["game_options"]["location_check_points"]
|
||||
self.server_password = savedata["game_options"]["server_password"]
|
||||
self.password = savedata["game_options"]["password"]
|
||||
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
|
||||
self.release_mode = savedata["game_options"]["release_mode"]
|
||||
self.remaining_mode = savedata["game_options"]["remaining_mode"]
|
||||
self.collect_mode = savedata["game_options"]["collect_mode"]
|
||||
self.item_cheat = savedata["game_options"]["item_cheat"]
|
||||
@@ -631,8 +631,6 @@ class Context:
|
||||
|
||||
def _set_options(self, server_options: dict):
|
||||
for key, value in server_options.items():
|
||||
if key == "forfeit_mode":
|
||||
key = "release_mode"
|
||||
data_type = self.simple_options.get(key, None)
|
||||
if data_type is not None:
|
||||
if value not in {False, True, None}: # some can be boolean OR text, such as password
|
||||
@@ -1347,6 +1345,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_missing(self, filter_text="") -> bool:
|
||||
"""List all missing location checks from the server's perspective.
|
||||
Can be given text, which will be used as filter."""
|
||||
@@ -1356,7 +1355,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
if filter_text:
|
||||
names = [name for name in names if filter_text in name]
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
names = [name for name in names if name in location_groups[filter_text]]
|
||||
else:
|
||||
names = [name for name in names if filter_text in name]
|
||||
texts = [f'Missing: {name}' for name in names]
|
||||
if filter_text:
|
||||
texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.")
|
||||
@@ -1367,6 +1370,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output("No missing location checks found.")
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_checked(self, filter_text="") -> bool:
|
||||
"""List all done location checks from the server's perspective.
|
||||
Can be given text, which will be used as filter."""
|
||||
@@ -1376,7 +1380,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if locations:
|
||||
names = [self.ctx.location_names[location] for location in locations]
|
||||
if filter_text:
|
||||
names = [name for name in names if filter_text in name]
|
||||
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
|
||||
if filter_text in location_groups: # location group name
|
||||
names = [name for name in names if name in location_groups[filter_text]]
|
||||
else:
|
||||
names = [name for name in names if filter_text in name]
|
||||
texts = [f'Checked: {name}' for name in names]
|
||||
if filter_text:
|
||||
texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.")
|
||||
@@ -1839,6 +1847,11 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
|
||||
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
|
||||
if new_status == ClientStatus.CLIENT_GOAL:
|
||||
ctx.on_goal_achieved(client)
|
||||
# if player has yet to ever connect to the server, they will not be in client_game_state
|
||||
if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL
|
||||
for player in ctx.player_names
|
||||
if player[0] == client.team and player[1] != client.slot):
|
||||
ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!")
|
||||
|
||||
ctx.client_game_state[client.team, client.slot] = new_status
|
||||
ctx.on_client_status_change(client.team, client.slot)
|
||||
@@ -2092,8 +2105,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
if full_name.isnumeric():
|
||||
location, usable, response = int(full_name), True, None
|
||||
elif self.ctx.location_names_for_game(game) is not None:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
|
||||
elif game in self.ctx.all_location_and_group_names:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game])
|
||||
else:
|
||||
self.output("Can't look up location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
@@ -2101,6 +2114,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
if usable:
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
||||
hints = []
|
||||
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
||||
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
if hints:
|
||||
|
||||
27
Options.py
27
Options.py
@@ -7,6 +7,7 @@ import math
|
||||
import numbers
|
||||
import random
|
||||
import typing
|
||||
import enum
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -20,6 +21,15 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
class Visibility(enum.IntFlag):
|
||||
none = 0b0000
|
||||
template = 0b0001
|
||||
simple_ui = 0b0010 # show option in simple menus, such as player-options
|
||||
complex_ui = 0b0100 # show option in complex menus, such as weighted-options
|
||||
spoiler = 0b1000
|
||||
all = 0b1111
|
||||
|
||||
|
||||
class AssembleOptions(abc.ABCMeta):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
options = attrs["options"] = {}
|
||||
@@ -102,6 +112,7 @@ T = typing.TypeVar('T')
|
||||
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
value: T
|
||||
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
||||
visibility = Visibility.all
|
||||
|
||||
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
||||
# Handled in get_option_name()
|
||||
@@ -1115,6 +1126,17 @@ class ItemLinks(OptionList):
|
||||
link.setdefault("link_replacement", None)
|
||||
|
||||
|
||||
class Removed(FreeText):
|
||||
"""This Option has been Removed."""
|
||||
default = ""
|
||||
visibility = Visibility.none
|
||||
|
||||
def __init__(self, value: str):
|
||||
if value:
|
||||
raise Exception("Option removed, please update your options file.")
|
||||
super().__init__(value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerGameCommonOptions(CommonOptions):
|
||||
local_items: LocalItems
|
||||
@@ -1170,7 +1192,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints
|
||||
all_options: typing.Dict[str, AssembleOptions] = {
|
||||
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
|
||||
if option.visibility & Visibility.template
|
||||
}
|
||||
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
|
||||
16
SNIClient.py
16
SNIClient.py
@@ -564,16 +564,12 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||
try:
|
||||
for address, data in write_list:
|
||||
while data:
|
||||
# Divide the write into packets of 256 bytes.
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data[:256])
|
||||
address += 256
|
||||
data = data[256:]
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data)
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
except ConnectionClosed:
|
||||
return False
|
||||
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.5"
|
||||
__version__ = "0.4.6"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
from typing import List, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Blueprint, abort
|
||||
from flask import Blueprint, abort, url_for
|
||||
|
||||
import worlds.Files
|
||||
from .. import cache
|
||||
from ..models import Room, Seed
|
||||
|
||||
@@ -21,12 +22,30 @@ def room_info(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
|
||||
def supports_apdeltapatch(game: str):
|
||||
return game in worlds.Files.AutoPatchRegister.patch_types
|
||||
downloads = []
|
||||
for slot in sorted(room.seed.slots):
|
||||
if slot.data and not supports_apdeltapatch(slot.game):
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
elif slot.data:
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
return {
|
||||
"tracker": room.tracker,
|
||||
"players": get_players(room.seed),
|
||||
"last_port": room.last_port,
|
||||
"last_activity": room.last_activity,
|
||||
"timeout": room.timeout
|
||||
"timeout": room.timeout,
|
||||
"downloads": downloads,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import multiprocessing
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
from uuid import UUID
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
@@ -62,6 +63,16 @@ def autohost(config: dict):
|
||||
def keep_running():
|
||||
try:
|
||||
with Locker("autohost"):
|
||||
# delete unowned user-content
|
||||
with db_session:
|
||||
# >>> bool(uuid.UUID(int=0))
|
||||
# True
|
||||
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
|
||||
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
|
||||
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
|
||||
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
|
||||
if rooms or seeds or slots:
|
||||
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
|
||||
run_guardian()
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
@@ -191,6 +202,6 @@ def run_guardian():
|
||||
guardian = threading.Thread(name="Guardian", target=guard)
|
||||
|
||||
|
||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
|
||||
from .customserver import run_server_process, get_static_server_data
|
||||
from .generate import gen_game
|
||||
|
||||
@@ -108,7 +108,10 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||
if e.__cause__:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"
|
||||
else:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e}"
|
||||
else:
|
||||
results[filename] = True
|
||||
return results, rolled_results
|
||||
|
||||
@@ -49,12 +49,6 @@ def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/games/<string:game>/player-settings")
|
||||
def player_settings(game: str):
|
||||
return redirect(url_for("player_options", game=game), 301)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
|
||||
@@ -45,7 +45,15 @@ def create():
|
||||
}
|
||||
|
||||
game_options = {}
|
||||
visible: typing.Set[str] = set()
|
||||
visible_weighted: typing.Set[str] = set()
|
||||
|
||||
for option_name, option in all_options.items():
|
||||
if option.visibility & Options.Visibility.simple_ui:
|
||||
visible.add(option_name)
|
||||
if option.visibility & Options.Visibility.complex_ui:
|
||||
visible_weighted.add(option_name)
|
||||
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
|
||||
@@ -116,8 +124,6 @@ def create():
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
@@ -156,12 +162,23 @@ def create():
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
filtered_player_options = player_options
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(player_options, f, indent=2, separators=(',', ': '))
|
||||
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible_weighted
|
||||
}
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in game_options.values():
|
||||
for option in filtered_player_options["gameOptions"].values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
@@ -170,7 +187,7 @@ def create():
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": game_options,
|
||||
"gameSettings": filtered_player_options["gameOptions"],
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<br /><br />
|
||||
|
||||
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||
You'll need to upload either a config file or a zip file containing one more config files.
|
||||
You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
|
||||
<br /><br />
|
||||
|
||||
If you have already generated a game and just need to host it, this site can<br />
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<th class="center">Players</th>
|
||||
<th>Created (UTC)</th>
|
||||
<th>Last Activity (UTC)</th>
|
||||
<th>Mark for deletion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -35,6 +36,7 @@
|
||||
<td>{{ room.seed.slots|length }}</td>
|
||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -51,6 +53,7 @@
|
||||
<th>Seed</th>
|
||||
<th class="center">Players</th>
|
||||
<th>Created (UTC)</th>
|
||||
<th>Mark for deletion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,6 +63,7 @@
|
||||
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
|
||||
</td>
|
||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -7,7 +7,7 @@ import zipfile
|
||||
import zlib
|
||||
|
||||
from io import BytesIO
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from flask import request, flash, redirect, url_for, session, render_template, abort
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
@@ -219,3 +219,29 @@ def user_content():
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
|
||||
return render_template("userContent.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
|
||||
@app.route("/disown_seed/<suuid:seed>", methods=["GET"])
|
||||
def disown_seed(seed):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
return abort(404)
|
||||
if seed.owner != session["_id"]:
|
||||
return abort(403)
|
||||
|
||||
seed.owner = 0
|
||||
|
||||
return redirect(url_for("user_content"))
|
||||
|
||||
|
||||
@app.route("/disown_room/<suuid:room>", methods=["GET"])
|
||||
def disown_room(room):
|
||||
room = Room.get(id=room)
|
||||
if not room:
|
||||
return abort(404)
|
||||
if room.owner != session["_id"]:
|
||||
return abort(403)
|
||||
|
||||
room.owner = 0
|
||||
|
||||
return redirect(url_for("user_content"))
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
# Archipelago Settings API
|
||||
|
||||
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
|
||||
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
|
||||
host.yaml. For the player options / player yamls see [options api.md](options api.md).
|
||||
|
||||
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
|
||||
as well as the predefined `host.yaml` in the repository.
|
||||
|
||||
@@ -380,11 +380,6 @@ from BaseClasses import Location
|
||||
|
||||
class MyGameLocation(Location):
|
||||
game: str = "My Game"
|
||||
|
||||
# override constructor to automatically mark event locations as such
|
||||
def __init__(self, player: int, name="", code=None, parent=None) -> None:
|
||||
super(MyGameLocation, self).__init__(player, name, code, parent)
|
||||
self.event = code is None
|
||||
```
|
||||
|
||||
in your `__init__.py` or your `locations.py`.
|
||||
|
||||
18
kvui.py
18
kvui.py
@@ -740,15 +740,17 @@ class KivyJSONtoTextParser(JSONtoTextParser):
|
||||
|
||||
def _handle_item_name(self, node: JSONMessagePart):
|
||||
flags = node.get("flags", 0)
|
||||
item_types = []
|
||||
if flags & 0b001: # advancement
|
||||
itemtype = "progression"
|
||||
elif flags & 0b010: # useful
|
||||
itemtype = "useful"
|
||||
elif flags & 0b100: # trap
|
||||
itemtype = "trap"
|
||||
else:
|
||||
itemtype = "normal"
|
||||
node.setdefault("refs", []).append("Item Class: " + itemtype)
|
||||
item_types.append("progression")
|
||||
if flags & 0b010: # useful
|
||||
item_types.append("useful")
|
||||
if flags & 0b100: # trap
|
||||
item_types.append("trap")
|
||||
if not item_types:
|
||||
item_types.append("normal")
|
||||
|
||||
node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types))
|
||||
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
|
||||
|
||||
def _handle_player_id(self, node: JSONMessagePart):
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# What is this file?
|
||||
# This file contains options which allow you to configure your multiworld experience while allowing others
|
||||
# to play how they want as well.
|
||||
|
||||
# How do I use it?
|
||||
# The options in this file are weighted. This means the higher number you assign to a value, the more
|
||||
# chances you have for that option to be chosen. For example, an option like this:
|
||||
#
|
||||
# map_shuffle:
|
||||
# on: 5
|
||||
# off: 15
|
||||
#
|
||||
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
|
||||
|
||||
# I've never seen a file like this before. What characters am I allowed to use?
|
||||
# This is a .yaml file. You are allowed to use most characters.
|
||||
# To test if your yaml is valid or not, you can use this website:
|
||||
# http://www.yamllint.com/
|
||||
|
||||
description: Template Name # Used to describe your yaml. Useful if you have multiple files
|
||||
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
#{player} will be replaced with the player's slot number.
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
# A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||
#
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is 0
|
||||
# Maximum value is 99
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
disabled: 0 # equivalent to 0
|
||||
normal: 50 # equivalent to 50
|
||||
extreme: 0 # equivalent to 99
|
||||
|
||||
accessibility:
|
||||
# Set rules for reachability of your items/locations.
|
||||
# Locations: ensure everything can be reached and acquired.
|
||||
# Items: ensure all logically relevant items can be acquired.
|
||||
# Minimal: ensure what is needed to reach your goal can be acquired.
|
||||
locations: 0
|
||||
items: 50
|
||||
minimal: 0
|
||||
|
||||
local_items:
|
||||
# Forces these items to be in their native world.
|
||||
[ ]
|
||||
|
||||
non_local_items:
|
||||
# Forces these items to be outside their native world.
|
||||
[ ]
|
||||
|
||||
start_inventory:
|
||||
# Start with these items.
|
||||
{ }
|
||||
|
||||
start_hints:
|
||||
# Start with these item's locations prefilled into the !hint command.
|
||||
[ ]
|
||||
|
||||
start_location_hints:
|
||||
# Start with these locations and their item prefilled into the !hint command
|
||||
[ ]
|
||||
|
||||
exclude_locations:
|
||||
# Prevent these locations from having an important item
|
||||
[ ]
|
||||
|
||||
priority_locations:
|
||||
# Prevent these locations from having an unimportant item
|
||||
[ ]
|
||||
|
||||
item_links:
|
||||
# Share part of your item pool with other players.
|
||||
[ ]
|
||||
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
|
||||
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
|
||||
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
|
||||
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
|
||||
# Other players items are placed into your world under HMG logic
|
||||
dark_room_logic: # Logic for unlit dark rooms
|
||||
lamp: 50 # require the Lamp for these rooms to be considered accessible.
|
||||
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
|
||||
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
|
||||
restrict_dungeon_item_on_boss: # aka ambrosia boss items
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
bigkey_shuffle: # Big Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
smallkey_shuffle: # Small Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
universal: 0
|
||||
start_with: 0
|
||||
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
|
||||
off: 50
|
||||
on: 0
|
||||
compass_shuffle: # Compass Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
map_shuffle: # Map Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
default: 0 # Show when compass is picked up if the compass itself is shuffled
|
||||
off: 0 # Never show item count in dungeons
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
crossed: 0 # Less strict than full
|
||||
insanity: 0 # Very few grouping rules. Good luck
|
||||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
|
||||
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
|
||||
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||
open_pyramid:
|
||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||
open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||
closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
0: 0
|
||||
5: 50
|
||||
10: 50
|
||||
15: 0
|
||||
20: 0
|
||||
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
100: 0 #No extra
|
||||
150: 50 #Half the required will be added as extra
|
||||
200: 0 #There are the double of the required ones available.
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 0
|
||||
30: 50
|
||||
40: 0
|
||||
50: 0
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
15: 0
|
||||
20: 50
|
||||
30: 0
|
||||
40: 0
|
||||
50: 0
|
||||
crystals_needed_for_gt: # Crystals required to open GT
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0 # any valid number, weighted towards the lower end
|
||||
random-middle: 0 # any valid number, weighted towards the central range
|
||||
random-high: 0 # any valid number, weighted towards the higher end
|
||||
crystals_needed_for_ganon: # Crystals required to hurt Ganon
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
mode:
|
||||
standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
|
||||
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
|
||||
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
|
||||
retro_bow:
|
||||
on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees.
|
||||
off: 50
|
||||
retro_caves:
|
||||
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
|
||||
off: 50
|
||||
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
|
||||
'on': 50
|
||||
'off': 0
|
||||
full: 0
|
||||
scams: # If on, these Merchants will no longer tell you what they're selling.
|
||||
'off': 50
|
||||
'king_zora': 0
|
||||
'bottle_merchant': 0
|
||||
'all': 0
|
||||
swordless:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
|
||||
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
|
||||
tile_shuffle: # Randomize the tile layouts in flying tile rooms
|
||||
on: 0
|
||||
off: 50
|
||||
misery_mire_medallion: # required medallion to open Misery Mire front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
### Enemizer Section ###
|
||||
boss_shuffle:
|
||||
none: 50 # Vanilla bosses
|
||||
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
|
||||
full: 0 # 3 bosses can occur twice
|
||||
chaos: 0 # Any boss can appear any amount of times
|
||||
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
|
||||
enemy_shuffle: # Randomize enemy placement
|
||||
on: 0
|
||||
off: 50
|
||||
killable_thieves: # Make thieves killable
|
||||
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
|
||||
off: 50
|
||||
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
|
||||
on: 0
|
||||
off: 50
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
hard: 0 # Enemies have increased health
|
||||
expert: 0 # Enemies have greatly increased health
|
||||
pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
### Beemizer ###
|
||||
# can add weights for any whole number between 0 and 100
|
||||
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No junk fill items are replaced (Beemizer is off)
|
||||
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
|
||||
beemizer_trap_chance:
|
||||
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
|
||||
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
|
||||
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
|
||||
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
|
||||
100: 0 # All beemizer replacements are traps
|
||||
### Shop Settings ###
|
||||
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_price_modifier: # Percentage modifier for shuffled item prices in shops
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
400: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
100: 50
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||
f: 0 # Generate new default inventories for every shop independently
|
||||
i: 0 # Shuffle default inventories of the shops around
|
||||
p: 0 # Randomize the prices of the items in shop inventories
|
||||
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
|
||||
ip: 0 # Shuffle inventories and randomize prices
|
||||
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
|
||||
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
|
||||
# You can add more combos
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
|
||||
b: 0 # shuffle "bonk" prize packs
|
||||
bg: 0 # shuffle both
|
||||
timer:
|
||||
none: 50 # No timer will be displayed.
|
||||
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
|
||||
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
|
||||
ohko: 0 # Timer always at zero. Permanent OHKO.
|
||||
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
|
||||
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
|
||||
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
|
||||
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
|
||||
10: 50
|
||||
20: 0
|
||||
30: 0
|
||||
60: 0
|
||||
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
|
||||
-2: 50
|
||||
1: 0
|
||||
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
|
||||
1: 0
|
||||
2: 50
|
||||
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
|
||||
4: 50
|
||||
10: 0
|
||||
15: 0
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
# rom options section
|
||||
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
|
||||
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
|
||||
on: 0
|
||||
off: 1
|
||||
on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts.
|
||||
on: 1
|
||||
off: 0
|
||||
on_enter: # Random sprite on underworld entry. Note that entering hobo counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_exit: # Random sprite on underworld exit. Exiting hobo does not count.
|
||||
on: 0
|
||||
off: 1
|
||||
on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless.
|
||||
on: 0
|
||||
off: 1
|
||||
on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_bonk: # Random sprite on bonk.
|
||||
on: 0
|
||||
off: 1
|
||||
on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time.
|
||||
on: 0
|
||||
off: 1
|
||||
use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool
|
||||
on: 0
|
||||
off: 1
|
||||
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
|
||||
# - link
|
||||
# - pride link
|
||||
# - penguin link
|
||||
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
|
||||
sprite: # Enter the name of your preferred sprite and weight it appropriately
|
||||
random: 0
|
||||
randomonhit: 0 # Random sprite on hit
|
||||
randomonenter: 0 # Random sprite on entering the underworld.
|
||||
randomonexit: 0 # Random sprite on exiting the underworld.
|
||||
randomonslash: 0 # Random sprite on sword slashes
|
||||
randomonitem: 0 # Random sprite on getting items.
|
||||
randomonbonk: 0 # Random sprite on bonk.
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
|
||||
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
|
||||
music: # If "off", all in-game music will be disabled
|
||||
on: 50
|
||||
off: 0
|
||||
quickswap: # Enable switching items by pressing the L+R shoulder buttons
|
||||
on: 50
|
||||
off: 0
|
||||
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
|
||||
normal: 0 # original behavior (always visible)
|
||||
hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala
|
||||
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
|
||||
hide_both: 0 # Hide both under above circumstances
|
||||
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
|
||||
on: 50
|
||||
off: 0
|
||||
menuspeed: # Controls how fast the item menu opens and closes
|
||||
normal: 50
|
||||
instant: 0
|
||||
double: 0
|
||||
triple: 0
|
||||
quadruple: 0
|
||||
half: 0
|
||||
heartcolor: # Controls the color of your health hearts
|
||||
red: 50
|
||||
blue: 0
|
||||
green: 0
|
||||
yellow: 0
|
||||
random: 0
|
||||
heartbeep: # Controls the frequency of the low-health beeping
|
||||
double: 0
|
||||
normal: 50
|
||||
half: 0
|
||||
quarter: 0
|
||||
off: 0
|
||||
ow_palettes: # Change the colors of the overworld
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
uw_palettes: # Change the colors of caves and dungeons
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
hud_palettes: # Change the colors of the hud
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
sword_palettes: # Change the colors of swords
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
shield_palettes: # Change the colors of shields
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
|
||||
# triggers that replace options upon rolling certain options
|
||||
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||
trigger_disabled: 50
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
|
||||
death_link:
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
# Off by default, because it currently crashes on real hardware.
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
percentage: 0 # Set this to the percentage chance you want crosskeys
|
||||
- name: localcrosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
local_items: # Forces keys to be local to your own world
|
||||
- "Small Keys"
|
||||
- "Big Keys"
|
||||
percentage: 0 # Set this to the percentage chance you want local crosskeys
|
||||
- name: enemizer
|
||||
options:
|
||||
A Link to the Past:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
basic: 1
|
||||
full: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
chaos: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
triggers:
|
||||
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||
- option_name: legacy_weapons
|
||||
option_result: randomized
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
- option_name: legacy_weapons
|
||||
option_result: assured
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
start_inventory:
|
||||
Progressive Sword: 1
|
||||
- option_name: legacy_weapons
|
||||
option_result: vanilla
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
plando_items:
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Pyramid Fairy - Left
|
||||
- Blacksmith
|
||||
- Link's Uncle
|
||||
- option_name: legacy_weapons
|
||||
option_result: swordless
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: on
|
||||
# end of legacy weapons block
|
||||
- option_name: enemy_damage # targets enemy_damage
|
||||
option_category: A Link to the Past
|
||||
option_result: shuffled # if it rolls shuffled
|
||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||
options: # then inserts these options
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Application settings / host.yaml interface using type hints.
|
||||
This is different from player settings.
|
||||
This is different from player options.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
@@ -221,7 +221,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> Li
|
||||
return items
|
||||
item = items.pop(0)
|
||||
multiworld.push_item(location, item, False)
|
||||
location.event = item.advancement
|
||||
|
||||
return items
|
||||
|
||||
@@ -489,7 +488,6 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
location.event = True
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
@@ -527,13 +525,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertFalse(locations[0].advancement)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertTrue(locations[1].advancement)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertTrue(locations[2].advancement)
|
||||
self.assertEqual(locations[3].item, basic_items[0])
|
||||
self.assertFalse(locations[3].event)
|
||||
self.assertFalse(locations[3].advancement)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
@@ -746,7 +744,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
for item in multiworld.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
self.assertFalse(item.location.advancement, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Any, Optional, Protocol
|
||||
from ..graphics import FillType_Drawable, FillType_Vec
|
||||
from ..graphics.texture import FillType_Drawable, FillType_Vec
|
||||
|
||||
|
||||
class FillType_BindCallback(Protocol):
|
||||
|
||||
@@ -121,7 +121,11 @@ class AutoLogicRegister(type):
|
||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||
start = time.perf_counter()
|
||||
ret = method(*args)
|
||||
if multiworld:
|
||||
with multiworld.observer(method, start):
|
||||
ret = method(*args)
|
||||
else:
|
||||
ret = method(*args)
|
||||
taken = time.perf_counter() - start
|
||||
if taken > 1.0:
|
||||
if player and multiworld:
|
||||
@@ -169,7 +173,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
_timed_call(stage_callable, multiworld, *args)
|
||||
_timed_call(stage_callable, multiworld, *args, multiworld=multiworld)
|
||||
|
||||
|
||||
class WebWorld:
|
||||
|
||||
@@ -64,7 +64,7 @@ class SuffixIdentifier:
|
||||
def __init__(self, *args: str):
|
||||
self.suffixes = args
|
||||
|
||||
def __call__(self, path: str):
|
||||
def __call__(self, path: str) -> bool:
|
||||
if isinstance(path, str):
|
||||
for suffix in self.suffixes:
|
||||
if path.endswith(suffix):
|
||||
|
||||
@@ -234,8 +234,11 @@ async def _run_game(rom: str):
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
try:
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
|
||||
|
||||
def launch() -> None:
|
||||
|
||||
@@ -43,7 +43,7 @@ an experience customized for their taste, and different players in the same mult
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
### What are recommended options to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
@@ -42,7 +42,7 @@ une expérience personnalisée à leur goût, et différents joueurs dans le mê
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-settings)
|
||||
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-options)
|
||||
|
||||
### Quels sont les paramètres recommandés pour s'initier à la rando ?
|
||||
Régler la difficulty_switch_a et réduire la vitesse des dragons rend les dragons plus faciles à éviter. Ajouter Calice à
|
||||
@@ -72,4 +72,4 @@ configuré pour le faire automatiquement.
|
||||
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
|
||||
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse> :<port> [mot de passe]`)
|
||||
|
||||
Appuyez sur Réinitialiser et commencez à jouer
|
||||
Appuyez sur Réinitialiser et commencez à jouer
|
||||
|
||||
@@ -2657,6 +2657,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
@@ -2815,6 +2819,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
|
||||
@@ -408,14 +408,16 @@ def create_inverted_regions(world, player):
|
||||
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
|
||||
'Turtle Rock Big Key Door']),
|
||||
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
|
||||
'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
@@ -253,10 +253,8 @@ def generate_itempool(world):
|
||||
region.locations.append(loc)
|
||||
|
||||
multiworld.push_item(loc, item_factory('Triforce', world), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
multiworld.get_location('Ganon', player).event = True
|
||||
multiworld.get_location('Ganon', player).locked = True
|
||||
event_pairs = [
|
||||
('Agahnim 1', 'Beat Agahnim 1'),
|
||||
@@ -273,7 +271,7 @@ def generate_itempool(world):
|
||||
location = multiworld.get_location(location_name, player)
|
||||
event = item_factory(event_name, world)
|
||||
multiworld.push_item(location, event, False)
|
||||
location.event = location.locked = True
|
||||
location.locked = True
|
||||
|
||||
|
||||
# set up item pool
|
||||
|
||||
@@ -2,7 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
|
||||
FreeText
|
||||
FreeText, Removed
|
||||
|
||||
|
||||
class GlitchesRequired(Choice):
|
||||
@@ -716,9 +716,8 @@ class BeemizerTrapChance(BeemizerRange):
|
||||
display_name = "Beemizer Trap Chance"
|
||||
|
||||
|
||||
class AllowCollect(Toggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
Off by default, because it currently crashes on real hardware."""
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players."""
|
||||
display_name = "Allow Collection of checks for other players"
|
||||
|
||||
|
||||
@@ -796,4 +795,9 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud,
|
||||
|
||||
# removed:
|
||||
"goals": Removed,
|
||||
"smallkey_shuffle": Removed,
|
||||
"bigkey_shuffle": Removed,
|
||||
}
|
||||
|
||||
@@ -336,13 +336,15 @@ def create_regions(world, player):
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
@@ -4,7 +4,7 @@ import Utils
|
||||
import worlds.Files
|
||||
|
||||
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||
RANDOMIZERBASEHASH: str = "35d010bc148e0ea0ee68e81e330223f1"
|
||||
RANDOMIZERBASEHASH: str = "8704fb9b9fa4fad52d4d2f9a95fb5360"
|
||||
ROM_PLAYER_LIMIT: int = 255
|
||||
|
||||
import io
|
||||
@@ -868,11 +868,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
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.
|
||||
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
|
||||
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
|
||||
elif room_id == 0x004a and world.fix_palaceofdarkness_exit[player]:
|
||||
elif room_id == 0x004a and local_world.fix_palaceofdarkness_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
|
||||
elif room_id == 0x00d6 and world.fix_trock_exit[player]:
|
||||
elif room_id == 0x00d6 and local_world.fix_trock_exit:
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
|
||||
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
|
||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
|
||||
@@ -1674,14 +1674,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x4E3BB, 0xEB)
|
||||
|
||||
# fix trock doors for reverse entrances
|
||||
if world.fix_trock_doors[player]:
|
||||
if local_world.fix_trock_doors:
|
||||
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
|
||||
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
|
||||
# included unconditionally in base2current
|
||||
# rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
|
||||
else:
|
||||
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
||||
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
||||
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])
|
||||
@@ -2397,6 +2397,9 @@ def write_strings(rom, world, player):
|
||||
if hint_count:
|
||||
locations = world.find_items_in_locations(items_to_hint, player, True)
|
||||
local_random.shuffle(locations)
|
||||
# make locked locations less likely to appear as hint,
|
||||
# chances are the lock means the player already knows.
|
||||
locations.sort(key=lambda sorting_location: not sorting_location.locked)
|
||||
for x in range(min(hint_count, len(locations))):
|
||||
this_location = locations.pop()
|
||||
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
|
||||
|
||||
@@ -279,6 +279,9 @@ def global_rules(world, player):
|
||||
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
|
||||
)
|
||||
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
|
||||
@@ -477,7 +480,6 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(world.get_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
@@ -487,6 +489,13 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.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(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
||||
|
||||
if not world.worlds[player].fix_trock_doors:
|
||||
add_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
if world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
||||
@@ -1184,7 +1193,6 @@ def set_trock_key_rules(world, player):
|
||||
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
|
||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||
location.place_locked_item(item)
|
||||
location.event = True
|
||||
toss_junk_item(world, player)
|
||||
|
||||
if world.accessibility[player] != 'locations':
|
||||
|
||||
@@ -261,6 +261,10 @@ class ALTTPWorld(World):
|
||||
self.dungeons = {}
|
||||
self.waterfall_fairy_bottle_fill = "Bottle"
|
||||
self.pyramid_fairy_bottle_fill = "Bottle"
|
||||
self.fix_trock_doors = None
|
||||
self.fix_skullwoods_exit = None
|
||||
self.fix_palaceofdarkness_exit = None
|
||||
self.fix_trock_exit = None
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -280,6 +284,15 @@ class ALTTPWorld(World):
|
||||
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']
|
||||
|
||||
# fairy bottle fills
|
||||
bottle_options = [
|
||||
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
||||
@@ -345,42 +358,43 @@ class ALTTPWorld(World):
|
||||
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
multiworld = self.multiworld
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
if multiworld.mode[player] != 'inverted':
|
||||
create_regions(multiworld, player)
|
||||
else:
|
||||
create_inverted_regions(world, player)
|
||||
create_shops(world, player)
|
||||
create_inverted_regions(multiworld, player)
|
||||
create_shops(multiworld, player)
|
||||
self.create_dungeons()
|
||||
|
||||
if world.glitches_required[player] not in ["no_glitches", "minor_glitches"] and world.entrance_shuffle[player] in \
|
||||
{"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"}:
|
||||
world.fix_fake_world[player] = False
|
||||
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
|
||||
multiworld.entrance_shuffle[player] in [
|
||||
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
|
||||
multiworld.fix_fake_world[player] = False
|
||||
|
||||
# seeded entrance shuffle
|
||||
old_random = world.random
|
||||
world.random = random.Random(self.er_seed)
|
||||
old_random = multiworld.random
|
||||
multiworld.random = random.Random(self.er_seed)
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
mark_light_world_regions(world, player)
|
||||
if multiworld.mode[player] != 'inverted':
|
||||
link_entrances(multiworld, player)
|
||||
mark_light_world_regions(multiworld, player)
|
||||
for region_name, entrance_name in indirect_connections_not_inverted.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
else:
|
||||
link_inverted_entrances(world, player)
|
||||
mark_dark_world_regions(world, player)
|
||||
link_inverted_entrances(multiworld, player)
|
||||
mark_dark_world_regions(multiworld, player)
|
||||
for region_name, entrance_name in indirect_connections_inverted.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
|
||||
world.random = old_random
|
||||
plando_connect(world, player)
|
||||
multiworld.random = old_random
|
||||
plando_connect(multiworld, player)
|
||||
|
||||
for region_name, entrance_name in indirect_connections.items():
|
||||
world.register_indirect_condition(world.get_region(region_name, player),
|
||||
world.get_entrance(entrance_name, player))
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
|
||||
multiworld.get_entrance(entrance_name, player))
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False):
|
||||
item_name = item.name
|
||||
|
||||
@@ -47,12 +47,12 @@ wählen können!
|
||||
|
||||
### Wo bekomme ich so eine YAML-Datei her?
|
||||
|
||||
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen
|
||||
Die [Player Options](/games/A Link to the Past/player-options) Seite auf der Website ermöglicht das einfache Erstellen
|
||||
und Herunterladen deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
|
||||
|
||||
### Deine YAML-Datei ist gewichtet!
|
||||
|
||||
Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
|
||||
Die **Player Options** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
|
||||
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
|
||||
|
||||
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen. So kann man
|
||||
|
||||
@@ -59,7 +59,7 @@ de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
|
||||
configuración personal y descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
@@ -86,7 +86,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
|
||||
|
||||
## Generar una partida para un jugador
|
||||
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
|
||||
click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
|
||||
|
||||
@@ -60,7 +60,7 @@ peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
|
||||
paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Configuration avancée du fichier YAML
|
||||
@@ -87,7 +87,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options,
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
|
||||
et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
@@ -207,4 +207,4 @@ Le logiciel recommandé pour l'auto-tracking actuellement est
|
||||
3. Sélectionnez votre appareil SNES dans la liste déroulante.
|
||||
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
|
||||
5. Cliquez sur le bouton **Start Autotracking**
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
||||
|
||||
@@ -101,20 +101,20 @@ class TestDeathMountain(TestInvertedOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Bottom Left", False, []],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Left", False, []],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Right", False, []],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
])
|
||||
@@ -177,7 +177,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -185,7 +185,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -201,7 +201,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
])
|
||||
@@ -1,4 +1,7 @@
|
||||
item_table = (
|
||||
'An Old GeoCities Profile',
|
||||
'Very Funny Joke',
|
||||
'Motivational Video',
|
||||
'Staples Easy Button',
|
||||
'One Million Dollars',
|
||||
'Replica Master Sword',
|
||||
@@ -13,7 +16,7 @@ item_table = (
|
||||
'2012 Magic the Gathering Core Set Starter Box',
|
||||
'Poke\'mon Booster Pack',
|
||||
'USB Speakers',
|
||||
'Plastic Spork',
|
||||
'Eco-Friendly Spork',
|
||||
'Cheeseburger',
|
||||
'Brand New Car',
|
||||
'Hunting Knife',
|
||||
@@ -22,7 +25,7 @@ item_table = (
|
||||
'One-Up Mushroom',
|
||||
'Nokia N-GAGE',
|
||||
'2-Liter of Sprite',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!',
|
||||
'Can of Compressed Air',
|
||||
'Striped Kitten',
|
||||
'USB Power Adapter',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
from ..generic.Rules import set_rule
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class ArchipIDLELogic(LogicMixin):
|
||||
@@ -10,29 +9,20 @@ class ArchipIDLELogic(LogicMixin):
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
for i in range(16, 31):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 4)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 4)
|
||||
|
||||
for i in range(31, 51):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 10)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 10)
|
||||
|
||||
for i in range(51, 101):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 20)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 20)
|
||||
|
||||
for i in range(101, 201):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 40)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 40)
|
||||
|
||||
world.completion_condition[player] =\
|
||||
lambda state:\
|
||||
state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
from .Items import item_table
|
||||
from .Rules import set_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ArchipIDLEWebWorld(WebWorld):
|
||||
@@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld):
|
||||
|
||||
class ArchipIDLEWorld(World):
|
||||
"""
|
||||
An idle game which sends a check every thirty seconds, up to two hundred checks.
|
||||
An idle game which sends a check every thirty to sixty seconds, up to two hundred checks.
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 5
|
||||
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
@@ -56,18 +55,40 @@ class ArchipIDLEWorld(World):
|
||||
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self):
|
||||
item_table_copy = list(item_table)
|
||||
self.multiworld.random.shuffle(item_table_copy)
|
||||
|
||||
item_pool = []
|
||||
for i in range(200):
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 40 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
item_pool = [
|
||||
ArchipIDLEItem(
|
||||
item_table[0],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[0]],
|
||||
self.player
|
||||
)
|
||||
item_pool.append(item)
|
||||
]
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[1],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[1]],
|
||||
self.player
|
||||
))
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[2],
|
||||
ItemClassification.filler,
|
||||
self.item_name_to_id[item_table[2]],
|
||||
self.player
|
||||
))
|
||||
|
||||
item_table_copy = list(item_table[3:])
|
||||
self.random.shuffle(item_table_copy)
|
||||
for i in range(119):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 9 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
[ArchipIDLE GitHub Releases Page](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
3. Enter the server address in the `Server Address` field and press enter
|
||||
4. Enter your slot name when prompted. This should be the same as the `name` you entered on the
|
||||
setting page above, or the `name` field in your yaml file.
|
||||
options page above, or the `name` field in your yaml file.
|
||||
5. Click the "Begin!" button.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# Guide de configuration d'ArchipIdle
|
||||
|
||||
## Rejoindre une partie MultiWorld
|
||||
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-settings)
|
||||
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-options)
|
||||
2. Ouvrez le client ArchipIDLE dans votre navigateur Web en :
|
||||
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
|
||||
- Téléchargez le client et exécutez-le localement à partir du
|
||||
[Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
|
||||
- Téléchargez le client et exécutez-le localement à partir du [Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
|
||||
3. Entrez l'adresse du serveur dans le champ `Server Address` et appuyez sur Entrée
|
||||
4. Entrez votre nom d'emplacement lorsque vous y êtes invité. Il doit être le même que le `name` que vous avez saisi sur le
|
||||
page de configuration ci-dessus, ou le champ `name` dans votre fichier yaml.
|
||||
|
||||
@@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple):
|
||||
class ChecksFinderAdvancement(Location):
|
||||
game: str = "ChecksFinder"
|
||||
|
||||
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
|
||||
super().__init__(player, name, address, parent)
|
||||
self.event = not address
|
||||
|
||||
|
||||
advancement_table = {
|
||||
"Tile 1": AdvData(81000, 'Board'),
|
||||
|
||||
@@ -29,5 +29,5 @@ placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III
|
||||
|
||||
## Où trouver le fichier de configuration ?
|
||||
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
|
||||
paramètres et de les exporter sous la forme d'un fichier.
|
||||
|
||||
@@ -294,7 +294,7 @@ barnacle_region = "Barnacle's Island Region"
|
||||
blue_region = "Blue's Beach Hut Region"
|
||||
blizzard_region = "Bizzard's Basecamp Region"
|
||||
|
||||
lake_orangatanga_region = "Lake_Orangatanga"
|
||||
lake_orangatanga_region = "Lake Orangatanga"
|
||||
kremwood_forest_region = "Kremwood Forest"
|
||||
cotton_top_cove_region = "Cotton-Top Cove"
|
||||
mekanos_region = "Mekanos"
|
||||
|
||||
@@ -201,7 +201,12 @@ class DKC3World(World):
|
||||
er_hint_data = {}
|
||||
for world_index in range(len(world_names)):
|
||||
for level_index in range(5):
|
||||
level_region = self.multiworld.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
|
||||
level_id: int = world_index * 5 + level_index
|
||||
|
||||
if level_id >= len(self.active_level_list):
|
||||
break
|
||||
|
||||
level_region = self.multiworld.get_region(self.active_level_list[level_id], self.player)
|
||||
for location in level_region.locations:
|
||||
er_hint_data[location.address] = world_names[world_index]
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class DLCqworld(World):
|
||||
self.precollect_coinsanity()
|
||||
locations_count = len([location
|
||||
for location in self.multiworld.get_locations(self.player)
|
||||
if not location.event])
|
||||
if not location.advancement])
|
||||
|
||||
items_to_exclude = [excluded_items
|
||||
for excluded_items in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Où se trouve la page des paramètres ?
|
||||
|
||||
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
|
||||
La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
|
||||
|
||||
|
||||
## Quel est l'effet de la randomisation sur ce jeu ?
|
||||
@@ -46,4 +46,4 @@ Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur
|
||||
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
|
||||
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
|
||||
|
||||
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
|
||||
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
|
||||
|
||||
@@ -18,7 +18,7 @@ Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Mult
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
|
||||
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest](/games/DLCQuest/player-options).
|
||||
|
||||
## Rejoindre une partie multi-monde
|
||||
|
||||
@@ -52,4 +52,4 @@ Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres d
|
||||
|
||||
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
|
||||
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
|
||||
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
|
||||
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
|
||||
|
||||
@@ -10,7 +10,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]:
|
||||
|
||||
|
||||
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [location.name for location in multiworld.get_locations() if not location.event]
|
||||
return [location.name for location in multiworld.get_locations() if not location.advancement]
|
||||
|
||||
|
||||
def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
@@ -38,5 +38,5 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
|
||||
|
||||
def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.advancement]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
@@ -90,7 +90,7 @@ def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typi
|
||||
if loc_name not in multiworld.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
if not location.event:
|
||||
if not location.advancement:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
|
||||
|
||||
@@ -2,27 +2,28 @@
|
||||
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who plans
|
||||
to edit their YAML file manually. This guide should take about 10 minutes to read.
|
||||
|
||||
If you would like to generate a basic, fully playable YAML without editing a file, then visit the settings page for the
|
||||
If you would like to generate a basic, fully playable YAML without editing a file, then visit the options page for the
|
||||
game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
|
||||
|
||||
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the
|
||||
game you would like.
|
||||
The options page can be found on the supported games page, just click the "Options Page" link under the name of the
|
||||
game you would like.
|
||||
|
||||
* Supported games page: [Archipelago Games List](/games)
|
||||
* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
|
||||
|
||||
Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options.
|
||||
The player settings page also has a link to download a full template file for that game which will have every option
|
||||
Clicking on the "Export Options" button at the bottom-left will provide you with a pre-filled YAML with your options.
|
||||
The player options page also has a link to download a full template file for that game which will have every option
|
||||
possible for the game including some that don't display correctly on the site.
|
||||
|
||||
## YAML Overview
|
||||
|
||||
The Archipelago system generates games using player configuration files as input. These are going to be YAML files and
|
||||
each world will have one of these containing their custom settings for the game that world will play.
|
||||
each world will have one of these containing their custom options for the game that world will play.
|
||||
|
||||
## YAML Formatting
|
||||
|
||||
YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then
|
||||
different levels of `nested` nodes that the generator reads in order to determine your settings.
|
||||
different levels of `nested` nodes that the generator reads in order to determine your options.
|
||||
|
||||
To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with
|
||||
whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text
|
||||
@@ -53,13 +54,13 @@ so `option_one_setting_one` is guaranteed to occur.
|
||||
|
||||
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
|
||||
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
|
||||
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
|
||||
adding more randomness and "mystery" to your options. Every configurable setting supports weights.
|
||||
|
||||
## Root Options
|
||||
|
||||
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
|
||||
options or in some cases nested within other nested options. The only options that should exist in root
|
||||
are `description`, `name`, `game`, `requires`, and the name of the games you want settings for.
|
||||
are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
|
||||
|
||||
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
|
||||
using this to detail the intention of the file.
|
||||
@@ -79,15 +80,15 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
|
||||
|
||||
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
|
||||
is good for detailing the version of Archipelago this YAML was prepared for as, if it is rolled on an older version,
|
||||
settings may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
|
||||
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
|
||||
here to ensure it will be used is good practice.
|
||||
|
||||
## Game Options
|
||||
|
||||
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
|
||||
One of your root options will be the name of the game you would like to populate with options. Since it is possible to
|
||||
give a weight to any option, it is possible to have one file that can generate a seed for you where you don't know which
|
||||
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
|
||||
settings. If a game can be rolled it **must** have a settings section even if it is empty.
|
||||
settings. If a game can be rolled it **must** have an options section even if it is empty.
|
||||
|
||||
### Universal Game Options
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ about 5 minutes to read.
|
||||
|
||||
## What are triggers?
|
||||
|
||||
Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
|
||||
Triggers allow you to customize your game options by allowing you to define one or many options which only occur under
|
||||
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
|
||||
can do with triggers is the [custom mercenary mode YAML
|
||||
](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was
|
||||
@@ -148,4 +148,4 @@ In this example, if the `start_location` option rolls `landing_site`, only a sta
|
||||
If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball.
|
||||
|
||||
Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will
|
||||
replace that value within the dict.
|
||||
replace that value within the dict.
|
||||
|
||||
55
worlds/hk/GodhomeData.py
Normal file
55
worlds/hk/GodhomeData.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from functools import partial
|
||||
|
||||
|
||||
godhome_event_names = ["Godhome_Flower_Quest", "Defeated_Pantheon_5", "GG_Atrium_Roof", "Defeated_Pantheon_1", "Defeated_Pantheon_2", "Defeated_Pantheon_3", "Opened_Pantheon_4", "Defeated_Pantheon_4", "GG_Atrium", "Hit_Pantheon_5_Unlock_Orb", "GG_Workshop", "Can_Damage_Crystal_Guardian", 'Defeated_Any_Soul_Warrior', "Defeated_Colosseum_3", "COMBAT[Radiance]", "COMBAT[Pantheon_1]", "COMBAT[Pantheon_2]", "COMBAT[Pantheon_3]", "COMBAT[Pantheon_4]", "COMBAT[Pantheon_5]", "COMBAT[Colosseum_3]", 'Warp-Junk_Pit_to_Godhome', 'Bench-Godhome_Atrium', 'Bench-Hall_of_Gods', "GODTUNERUNLOCK", "GG_Waterways", "Warp-Godhome_to_Junk_Pit", "NAILCOMBAT", "BOSS", "AERIALMINIBOSS"]
|
||||
|
||||
|
||||
def set_godhome_rules(hk_world, hk_set_rule):
|
||||
player = hk_world.player
|
||||
fn = partial(hk_set_rule, hk_world)
|
||||
|
||||
required_events = {
|
||||
"Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player),
|
||||
|
||||
"Defeated_Pantheon_5": lambda state: state.has('GG_Atrium_Roof', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and ((state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player) and state.has('COMBAT[Radiance]', player))),
|
||||
"GG_Atrium_Roof": lambda state: state.has('GG_Atrium', player) and state.has('Hit_Pantheon_5_Unlock_Orb', player) and state.has('LEFTCLAW', player),
|
||||
|
||||
"Defeated_Pantheon_1": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Gruz_Mother', player) and state.has('Defeated_False_Knight', player) and (state.has('Fungus1_29[left1]', player) or state.has('Fungus1_29[right1]', player)) and state.has('Defeated_Hornet_1', player) and state.has('Defeated_Gorb', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_Any_Soul_Warrior', player) and state.has('Defeated_Brooding_Mawlek', player))),
|
||||
"Defeated_Pantheon_2": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Xero', player) and state.has('Defeated_Crystal_Guardian', player) and state.has('Defeated_Soul_Master', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Mantis_Lords', player) and state.has('Defeated_Marmu', player) and state.has('Defeated_Nosk', player) and state.has('Defeated_Flukemarm', player) and state.has('Defeated_Broken_Vessel', player))),
|
||||
"Defeated_Pantheon_3": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Hive_Knight', player) and state.has('Defeated_Elder_Hu', player) and state.has('Defeated_Collector', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Grimm', player) and state.has('Defeated_Galien', player) and state.has('Defeated_Uumuu', player) and state.has('Defeated_Hornet_2', player))),
|
||||
"Opened_Pantheon_4": lambda state: state.has('GG_Atrium', player) and (state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player)),
|
||||
"Defeated_Pantheon_4": lambda state: state.has('GG_Atrium', player) and state.has('Opened_Pantheon_4', player) and ((state.has('Defeated_Enraged_Guardian', player) and state.has('Defeated_Broken_Vessel', player) and state.has('Defeated_No_Eyes', player) and state.has('Defeated_Traitor_Lord', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_False_Knight', player) and state.has('Defeated_Markoth', player) and state.has('Defeated_Watcher_Knights', player) and state.has('Defeated_Soul_Master', player))),
|
||||
"GG_Atrium": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) and (state.has('RIGHTCLAW', player) or state.has('WINGS', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player)) or state.has('GG_Workshop', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player) and state.has('WINGS', player)) or state.has('Bench-Godhome_Atrium', player),
|
||||
"Hit_Pantheon_5_Unlock_Orb": lambda state: state.has('GG_Atrium', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and (((state.has('Queen_Fragment', player) and state.has('King_Fragment', player) and state.has('Void_Heart', player)) and state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player))),
|
||||
"GG_Workshop": lambda state: state.has('GG_Atrium', player) or state.has('Bench-Hall_of_Gods', player),
|
||||
"Can_Damage_Crystal_Guardian": lambda state: state.has('UPSLASH', player) or state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) and (state.has('DREAMNAIL', player) and (state.has('SPELLS', player) or state.has('FOCUS', player) and state.has('Spore_Shroom', player) or state.has('Glowing_Womb', player)) or state.has('Weaversong', player)),
|
||||
'Defeated_Any_Soul_Warrior': lambda state: state.has('Defeated_Sanctum_Warrior', player) or state.has('Defeated_Elegant_Warrior', player) or state.has('Room_Colosseum_01[left1]', player) and state.has('Defeated_Colosseum_3', player),
|
||||
"Defeated_Colosseum_3": lambda state: state.has('Room_Colosseum_01[left1]', player) and state.has('Can_Replenish_Geo', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) or ((state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and state.has('WINGS', player))) and state.has('COMBAT[Colosseum_3]', player),
|
||||
|
||||
# MACROS
|
||||
"COMBAT[Radiance]": lambda state: (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_1]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_2]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player),
|
||||
"COMBAT[Pantheon_3]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_4]": lambda state: state.has('AERIALMINIBOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_5]": lambda state: state.has('AERIALMINIBOSS', player) and state.has('FOCUS', player) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Colosseum_3]": lambda state: state.has('BOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
|
||||
# MISC
|
||||
'Warp-Junk_Pit_to_Godhome': lambda state: state.has('GG_Waterways', player) and state.has('GODTUNERUNLOCK', player) and state.has('DREAMNAIL', player),
|
||||
'Bench-Godhome_Atrium': lambda state: state.has('GG_Atrium', player) and (state.has('RIGHTCLAW', player) and (state.has('RIGHTDASH', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player) or state.has('WINGS', player)) or state.has('LEFTCLAW', player) and state.has('WINGS', player)),
|
||||
'Bench-Hall_of_Gods': lambda state: state.has('GG_Workshop', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player))),
|
||||
|
||||
"GODTUNERUNLOCK": lambda state: state.count('SIMPLE', player) > 3,
|
||||
"GG_Waterways": lambda state: state.has('GG_Waterways[door1]', player) or state.has('GG_Waterways[right1]', player) and (state.has('LEFTSUPERDASH', player) or state.has('SWIM', player)) or state.has('Warp-Godhome_to_Junk_Pit', player),
|
||||
"Warp-Godhome_to_Junk_Pit": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) or state.has('GG_Atrium', player),
|
||||
|
||||
# COMBAT MACROS
|
||||
"NAILCOMBAT": lambda state: (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')),
|
||||
"BOSS": lambda state: state.count('SPELLS', player) > 1 and ((state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and state.has('NAILCOMBAT', player)),
|
||||
"AERIALMINIBOSS": lambda state: (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state.has('CYCLONE', player) or state.has('Great_Slash', player)),
|
||||
|
||||
}
|
||||
|
||||
for item, rule in required_events.items():
|
||||
fn(item, rule)
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Dict, Set, NamedTuple
|
||||
from .ExtractedData import items, logic_items, item_effects
|
||||
from .GodhomeData import godhome_event_names
|
||||
|
||||
item_table = {}
|
||||
|
||||
@@ -14,6 +15,9 @@ for i, (item_name, item_type) in enumerate(items.items(), start=0x1000000):
|
||||
item_table[item_name] = HKItemData(advancement=item_name in logic_items or item_name in item_effects,
|
||||
id=i, type=item_type)
|
||||
|
||||
for item_name in godhome_event_names:
|
||||
item_table[item_name] = HKItemData(advancement=True, id=None, type=None)
|
||||
|
||||
lookup_id_to_name: Dict[int, str] = {data.id: item_name for item_name, data in item_table.items()}
|
||||
lookup_type_to_names: Dict[str, Set[str]] = {}
|
||||
for item, item_data in item_table.items():
|
||||
|
||||
@@ -397,8 +397,8 @@ class Goal(Choice):
|
||||
option_hollowknight = 1
|
||||
option_siblings = 2
|
||||
option_radiance = 3
|
||||
# Client support exists for this, but logic is a nightmare
|
||||
# option_godhome = 4
|
||||
option_godhome = 4
|
||||
option_godhome_flower = 5
|
||||
default = 0
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
from ..AutoWorld import World
|
||||
from .GeneratedRules import set_generated_rules
|
||||
from .GodhomeData import set_godhome_rules
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
@@ -39,6 +40,7 @@ def hk_set_rule(hk_world: World, location: str, rule):
|
||||
def set_rules(hk_world: World):
|
||||
player = hk_world.player
|
||||
set_generated_rules(hk_world, hk_set_rule)
|
||||
set_godhome_rules(hk_world, hk_set_rule)
|
||||
|
||||
# Shop costs
|
||||
for location in hk_world.multiworld.get_locations(player):
|
||||
|
||||
@@ -307,6 +307,12 @@ class HKWorld(World):
|
||||
randomized = True
|
||||
_add("Elevator_Pass", "Elevator_Pass", randomized)
|
||||
|
||||
# check for any goal that godhome events are relevant to
|
||||
if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]:
|
||||
from .GodhomeData import godhome_event_names
|
||||
for item_name in godhome_event_names:
|
||||
_add(item_name, item_name, False)
|
||||
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
|
||||
loc = self.create_location(shop)
|
||||
@@ -431,6 +437,10 @@ class HKWorld(World):
|
||||
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
|
||||
elif goal == Goal.option_radiance:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
|
||||
elif goal == Goal.option_godhome:
|
||||
world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
|
||||
elif goal == Goal.option_godhome_flower:
|
||||
world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
|
||||
else:
|
||||
# Any goal
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
|
||||
|
||||
@@ -413,26 +413,31 @@ def set_rules(hylics2world):
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
|
||||
lambda state: paddle(state, player))
|
||||
|
||||
@@ -110,7 +110,11 @@ def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]):
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
|
||||
world.player).parent_region.add_exits([first_rooms[proper_stage].name])
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
if world.options.open_world:
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
|
||||
.parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
|
||||
|
||||
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
|
||||
|
||||
@@ -757,7 +757,7 @@ class Assembler:
|
||||
|
||||
def const(name: str, value: int) -> None:
|
||||
name = name.upper()
|
||||
assert name not in CONST_MAP
|
||||
assert name not in CONST_MAP or CONST_MAP[name] == value
|
||||
CONST_MAP[name] = value
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ from .locations.keyLocation import KeyLocation
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import LinksAwakeningLocation
|
||||
from ..Options import TrendyGame, Palette, MusicChangeCondition
|
||||
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
|
||||
|
||||
|
||||
# Function to generate a final rom, this patches the rom with all required patches
|
||||
@@ -97,7 +97,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
||||
assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have
|
||||
assembler.const("wSeashellsCount", 0xDB41)
|
||||
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
|
||||
assembler.const("wCustomMessage", 0xC0A0)
|
||||
|
||||
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
|
||||
@@ -243,6 +243,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
||||
patches.core.quickswap(rom, 1)
|
||||
elif settings.quickswap == 'b':
|
||||
patches.core.quickswap(rom, 0)
|
||||
|
||||
patches.core.addBootsControls(rom, ap_settings['boots_controls'])
|
||||
|
||||
|
||||
world_setup = logic.world_setup
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ class StartItem(DroppedKey):
|
||||
# We need to give something here that we can use to progress.
|
||||
# FEATHER
|
||||
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
|
||||
|
||||
MULTIWORLD = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -51,7 +51,7 @@ GiveItemFromChest:
|
||||
dw ChestBow ; CHEST_BOW
|
||||
dw ChestWithItem ; CHEST_HOOKSHOT
|
||||
dw ChestWithItem ; CHEST_MAGIC_ROD
|
||||
dw ChestWithItem ; CHEST_PEGASUS_BOOTS
|
||||
dw Boots ; CHEST_PEGASUS_BOOTS
|
||||
dw ChestWithItem ; CHEST_OCARINA
|
||||
dw ChestWithItem ; CHEST_FEATHER
|
||||
dw ChestWithItem ; CHEST_SHOVEL
|
||||
@@ -273,6 +273,13 @@ ChestMagicPowder:
|
||||
ld [$DB4C], a
|
||||
jp ChestWithItem
|
||||
|
||||
Boots:
|
||||
; We use DB6D to store which tunics we have available
|
||||
; ...and the boots
|
||||
ld a, [wCollectedTunics]
|
||||
or $04
|
||||
ld [wCollectedTunics], a
|
||||
jp ChestWithItem
|
||||
|
||||
Flippers:
|
||||
ld a, $01
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from .. import assembler
|
||||
from ..assembler import ASM
|
||||
from ..entranceInfo import ENTRANCE_INFO
|
||||
from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal
|
||||
from ..backgroundEditor import BackgroundEditor
|
||||
from .. import utils
|
||||
|
||||
from ...Options import BootsControls
|
||||
|
||||
def bugfixWrittingWrongRoomStatus(rom):
|
||||
# The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in
|
||||
@@ -391,7 +393,7 @@ OAMData:
|
||||
db $20, $20, $20, $00 ;I
|
||||
db $20, $28, $28, $00 ;M
|
||||
db $20, $30, $18, $00 ;E
|
||||
|
||||
|
||||
db $20, $70, $16, $00 ;D
|
||||
db $20, $78, $18, $00 ;E
|
||||
db $20, $80, $10, $00 ;A
|
||||
@@ -408,7 +410,7 @@ OAMData:
|
||||
db $68, $38, $%02x, $00 ;0
|
||||
db $68, $40, $%02x, $00 ;0
|
||||
db $68, $48, $%02x, $00 ;0
|
||||
|
||||
|
||||
""" % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True)
|
||||
# Lower line of credits roll into XX XX XX
|
||||
rom.patch(0x17, 0x0784, 0x082D, ASM("""
|
||||
@@ -425,7 +427,7 @@ OAMData:
|
||||
call updateOAM
|
||||
ld a, [$B001] ; seconds
|
||||
call updateOAM
|
||||
|
||||
|
||||
ld a, [$DB58] ; death count high
|
||||
call updateOAM
|
||||
ld a, [$DB57] ; death count low
|
||||
@@ -473,7 +475,7 @@ OAMData:
|
||||
db $68, $18, $40, $00 ;0
|
||||
db $68, $20, $40, $00 ;0
|
||||
db $68, $28, $40, $00 ;0
|
||||
|
||||
|
||||
""", 0x4784), fill_nop=True)
|
||||
|
||||
# Grab the "mostly" complete A-Z font
|
||||
@@ -539,6 +541,97 @@ OAMData:
|
||||
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
|
||||
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
|
||||
|
||||
def addBootsControls(rom, boots_controls: BootsControls):
|
||||
if boots_controls == BootsControls.option_vanilla:
|
||||
return
|
||||
consts = {
|
||||
"INVENTORY_PEGASUS_BOOTS": 0x8,
|
||||
"INVENTORY_POWER_BRACELET": 0x3,
|
||||
"UsePegasusBoots": 0x1705,
|
||||
"J_A": (1 << 4),
|
||||
"J_B": (1 << 5),
|
||||
"wAButtonSlot": 0xDB01,
|
||||
"wBButtonSlot": 0xDB00,
|
||||
"wPegasusBootsChargeMeter": 0xC14B,
|
||||
"hPressedButtonsMask": 0xCB
|
||||
}
|
||||
for c,v in consts.items():
|
||||
assembler.const(c, v)
|
||||
|
||||
BOOTS_START_ADDR = 0x11E8
|
||||
condition = {
|
||||
BootsControls.option_bracelet: """
|
||||
ld a, [hl]
|
||||
; Check if we are using the bracelet
|
||||
cp INVENTORY_POWER_BRACELET
|
||||
jr z, .yesBoots
|
||||
""",
|
||||
BootsControls.option_press_a: """
|
||||
; Check if we are using the A slot
|
||||
cp J_A
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
""",
|
||||
BootsControls.option_press_b: """
|
||||
; Check if we are using the B slot
|
||||
cp J_B
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
"""
|
||||
}[boots_controls.value]
|
||||
|
||||
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
|
||||
boots_code = assembler.ASM("""
|
||||
CheckBoots:
|
||||
; check if we own boots
|
||||
ld a, [wCollectedTunics]
|
||||
and $04
|
||||
; if not, move on to the next inventory item (shield)
|
||||
jr z, .out
|
||||
|
||||
; Check the B button
|
||||
ld hl, wBButtonSlot
|
||||
ld d, J_B
|
||||
call .maybeBoots
|
||||
|
||||
; Check the A button
|
||||
inc l ; l = wAButtonSlot - done this way to save a byte or two
|
||||
ld d, J_A
|
||||
call .maybeBoots
|
||||
|
||||
; If neither, reset charge meter and bail
|
||||
xor a
|
||||
ld [wPegasusBootsChargeMeter], a
|
||||
jr .out
|
||||
|
||||
.maybeBoots:
|
||||
; Check if we are holding this button even
|
||||
ldh a, [hPressedButtonsMask]
|
||||
and d
|
||||
ret z
|
||||
"""
|
||||
# Check the special condition (also loads the current item for button into a)
|
||||
+ condition +
|
||||
"""
|
||||
; Check if we are just using boots regularly
|
||||
cp INVENTORY_PEGASUS_BOOTS
|
||||
ret nz
|
||||
.yesBoots:
|
||||
; We're using boots! Do so.
|
||||
call UsePegasusBoots
|
||||
; If we return now we will go back into CheckBoots, we don't want that
|
||||
; We instead want to move onto the next item
|
||||
; but if we don't cleanup, the next "ret" will take us back there again
|
||||
; So we pop the return address off of the stack
|
||||
pop af
|
||||
.out:
|
||||
""", BOOTS_START_ADDR)
|
||||
|
||||
|
||||
|
||||
original_code = 'fa00dbfe08200ff0cbe6202805cd05171804afea4bc1fa01dbfe08200ff0cbe6102805cd05171804afea4bc1'
|
||||
rom.patch(0, BOOTS_START_ADDR, original_code, boots_code, fill_nop=True)
|
||||
|
||||
def addWarpImprovements(rom, extra_warps):
|
||||
# Patch in a warp icon
|
||||
tile = utils.createTileData( \
|
||||
@@ -739,4 +832,3 @@ success:
|
||||
exit:
|
||||
ret
|
||||
"""))
|
||||
|
||||
|
||||
@@ -60,13 +60,11 @@ class LinksAwakeningLocation(Location):
|
||||
|
||||
def __init__(self, player: int, region, ladxr_item):
|
||||
name = meta_to_name(ladxr_item.metadata)
|
||||
|
||||
self.event = ladxr_item.event is not None
|
||||
if self.event:
|
||||
name = ladxr_item.event
|
||||
|
||||
address = None
|
||||
if not self.event:
|
||||
|
||||
if ladxr_item.event is not None:
|
||||
name = ladxr_item.event
|
||||
else:
|
||||
address = locations_to_id[name]
|
||||
super().__init__(player, name, address)
|
||||
self.parent_region = region
|
||||
|
||||
@@ -316,6 +316,21 @@ class Overworld(Choice, LADXROption):
|
||||
# [Disable] no music in the whole game""",
|
||||
# aesthetic=True),
|
||||
|
||||
class BootsControls(Choice):
|
||||
"""
|
||||
Adds additional button to activate Pegasus Boots (does nothing if you haven't picked up your boots!)
|
||||
[Vanilla] Nothing changes, you have to equip the boots to use them
|
||||
[Bracelet] Holding down the button for the bracelet also activates boots (somewhat like Link to the Past)
|
||||
[Press A] Holding down A activates boots
|
||||
[Press B] Holding down B activates boots
|
||||
"""
|
||||
display_name = "Boots Controls"
|
||||
option_vanilla = 0
|
||||
option_bracelet = 1
|
||||
option_press_a = 2
|
||||
option_press_b = 3
|
||||
|
||||
|
||||
class LinkPalette(Choice, LADXROption):
|
||||
"""
|
||||
Sets link's palette
|
||||
@@ -485,5 +500,5 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
||||
'music_change_condition': MusicChangeCondition,
|
||||
'nag_messages': NagMessages,
|
||||
'ap_title_screen': APTitleScreen,
|
||||
|
||||
'boots_controls': BootsControls,
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class LinksAwakeningWorld(World):
|
||||
# Place RAFT, other access events
|
||||
for region in regions:
|
||||
for loc in region.locations:
|
||||
if loc.event:
|
||||
if loc.address is None:
|
||||
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
|
||||
|
||||
# Connect Windfish -> Victory
|
||||
|
||||
@@ -63,7 +63,7 @@ class LingoWorld(World):
|
||||
self.player_logic = LingoPlayerLogic(self)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self, self.player_logic)
|
||||
create_regions(self)
|
||||
|
||||
def create_items(self):
|
||||
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
||||
|
||||
@@ -4,7 +4,6 @@ from BaseClasses import Entrance, ItemClassification, Region
|
||||
from .datatypes import Room, RoomAndDoor
|
||||
from .items import LingoItem
|
||||
from .locations import LingoLocation
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .rules import lingo_can_use_entrance, make_location_lambda
|
||||
from .static_logic import ALL_ROOMS, PAINTINGS
|
||||
|
||||
@@ -12,14 +11,14 @@ if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region:
|
||||
def create_region(room: Room, world: "LingoWorld") -> Region:
|
||||
new_region = Region(room.name, world.player, world.multiworld)
|
||||
for location in player_logic.locations_by_room.get(room.name, {}):
|
||||
for location in world.player_logic.locations_by_room.get(room.name, {}):
|
||||
new_location = LingoLocation(world.player, location.name, location.code, new_region)
|
||||
new_location.access_rule = make_location_lambda(location, world, player_logic)
|
||||
new_location.access_rule = make_location_lambda(location, world)
|
||||
new_region.locations.append(new_location)
|
||||
if location.name in player_logic.event_loc_to_item:
|
||||
event_name = player_logic.event_loc_to_item[location.name]
|
||||
if location.name in world.player_logic.event_loc_to_item:
|
||||
event_name = world.player_logic.event_loc_to_item[location.name]
|
||||
event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
|
||||
new_location.place_locked_item(event_item)
|
||||
|
||||
@@ -27,22 +26,21 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
|
||||
|
||||
|
||||
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
|
||||
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
door: Optional[RoomAndDoor], world: "LingoWorld"):
|
||||
connection = Entrance(world.player, description, source_region)
|
||||
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic)
|
||||
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world)
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
|
||||
if door is not None:
|
||||
effective_room = target_region.name if door.room is None else door.room
|
||||
if door.door not in player_logic.item_by_door.get(effective_room, {}):
|
||||
for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
|
||||
if door.door not in world.player_logic.item_by_door.get(effective_room, {}):
|
||||
for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
|
||||
world.multiworld.register_indirect_condition(regions[region], connection)
|
||||
|
||||
|
||||
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic) -> None:
|
||||
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None:
|
||||
source_painting = PAINTINGS[warp_enter]
|
||||
target_painting = PAINTINGS[warp_exit]
|
||||
|
||||
@@ -50,11 +48,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str
|
||||
source_region = regions[source_painting.room]
|
||||
|
||||
entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)"
|
||||
connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world,
|
||||
player_logic)
|
||||
connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world)
|
||||
|
||||
|
||||
def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
def create_regions(world: "LingoWorld") -> None:
|
||||
regions = {
|
||||
"Menu": Region("Menu", world.player, world.multiworld)
|
||||
}
|
||||
@@ -64,7 +61,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
|
||||
# Instantiate all rooms as regions with their locations first.
|
||||
for room in ALL_ROOMS:
|
||||
regions[room.name] = create_region(room, world, player_logic)
|
||||
regions[room.name] = create_region(room, world)
|
||||
|
||||
# Connect all created regions now that they exist.
|
||||
for room in ALL_ROOMS:
|
||||
@@ -80,18 +77,17 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
else:
|
||||
entrance_name += f" (through {room.name} - {entrance.door.door})"
|
||||
|
||||
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
|
||||
player_logic)
|
||||
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world)
|
||||
|
||||
# Add the fake pilgrimage.
|
||||
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
|
||||
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
|
||||
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world)
|
||||
|
||||
if early_color_hallways:
|
||||
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
|
||||
|
||||
if painting_shuffle:
|
||||
for warp_enter, warp_exit in player_logic.painting_mapping.items():
|
||||
connect_painting(regions, warp_enter, warp_exit, world, player_logic)
|
||||
for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
|
||||
connect_painting(regions, warp_enter, warp_exit, world)
|
||||
|
||||
world.multiworld.regions += regions.values()
|
||||
|
||||
@@ -2,61 +2,58 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .datatypes import RoomAndDoor
|
||||
from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation
|
||||
from .player_logic import AccessRequirements, PlayerLocation
|
||||
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld"):
|
||||
if door is None:
|
||||
return True
|
||||
|
||||
effective_room = room if door.room is None else door.room
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world)
|
||||
|
||||
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"):
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world)
|
||||
|
||||
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
|
||||
satisfied_count = 0
|
||||
for access_req in player_logic.mastery_reqs:
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
for access_req in world.player_logic.mastery_reqs:
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world):
|
||||
satisfied_count += 1
|
||||
return satisfied_count >= world.options.mastery_achievements.value
|
||||
|
||||
|
||||
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"):
|
||||
counted_panels = 0
|
||||
state.update_reachable_regions(world.player)
|
||||
for region in state.reachable_regions[world.player]:
|
||||
for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []):
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
for access_req, panel_count in world.player_logic.counting_panel_reqs.get(region.name, []):
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world):
|
||||
counted_panels += panel_count
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
# THE MASTER has to be handled separately, because it has special access rules.
|
||||
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
|
||||
and lingo_can_use_mastery_location(state, world, player_logic):
|
||||
and lingo_can_use_mastery_location(state, world):
|
||||
counted_panels += 1
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld"):
|
||||
for req_room in access.rooms:
|
||||
if not state.can_reach(req_room, "Region", world.player):
|
||||
return False
|
||||
|
||||
for req_door in access.doors:
|
||||
if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic):
|
||||
if not _lingo_can_open_door(state, req_door.room, req_door.door, world):
|
||||
return False
|
||||
|
||||
if len(access.colors) > 0 and world.options.shuffle_colors:
|
||||
@@ -67,15 +64,14 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
|
||||
return True
|
||||
|
||||
|
||||
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld"):
|
||||
"""
|
||||
Determines whether a door can be opened
|
||||
"""
|
||||
if door not in player_logic.item_by_door.get(room, {}):
|
||||
return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic)
|
||||
if door not in world.player_logic.item_by_door.get(room, {}):
|
||||
return _lingo_can_satisfy_requirements(state, world.player_logic.door_reqs[room][door], world)
|
||||
|
||||
item_name = player_logic.item_by_door[room][door]
|
||||
item_name = world.player_logic.item_by_door[room][door]
|
||||
if item_name in PROGRESSIVE_ITEMS:
|
||||
progression = PROGRESSION_BY_ROOM[room][door]
|
||||
return state.has(item_name, world.player, progression.index)
|
||||
@@ -83,12 +79,12 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L
|
||||
return state.has(item_name, world.player)
|
||||
|
||||
|
||||
def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
if location.name == player_logic.mastery_location:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world, player_logic)
|
||||
def make_location_lambda(location: PlayerLocation, world: "LingoWorld"):
|
||||
if location.name == world.player_logic.mastery_location:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world)
|
||||
|
||||
if world.options.level_2_requirement > 1\
|
||||
and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location):
|
||||
return lambda state: lingo_can_use_level_2_location(state, world, player_logic)
|
||||
and (location.name == "Second Room - ANOTHER TRY" or location.name == world.player_logic.level_2_location):
|
||||
return lambda state: lingo_can_use_level_2_location(state, world)
|
||||
|
||||
return lambda state: lingo_can_use_location(state, location, world, player_logic)
|
||||
return lambda state: lingo_can_use_location(state, location, world)
|
||||
|
||||
@@ -593,6 +593,20 @@ class HealingFloorChance(Range):
|
||||
default = 16
|
||||
|
||||
|
||||
class InactiveExpGain(Choice):
|
||||
"""The rate at which characters not currently in the active party gain EXP.
|
||||
|
||||
Supported values: disabled, half, full
|
||||
Default value: disabled (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Inactive character EXP gain"
|
||||
option_disabled = 0
|
||||
option_half = 50
|
||||
option_full = 100
|
||||
default = option_disabled
|
||||
|
||||
|
||||
class InitialFloor(Range):
|
||||
"""The initial floor, where you begin your journey.
|
||||
|
||||
@@ -805,7 +819,7 @@ class ShufflePartyMembers(Toggle):
|
||||
false — all 6 optional party members are present in the cafe and can be recruited right away
|
||||
true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the
|
||||
multiworld; when one of these items is found, the corresponding party member is unlocked for you to use.
|
||||
While cave diving, you can add newly unlocked ones to your party by using the character items from the inventory
|
||||
While cave diving, you can add or remove unlocked party members by using the character items from the inventory
|
||||
Default value: false (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
@@ -838,6 +852,7 @@ class L2ACOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
gold_modifier: GoldModifier
|
||||
healing_floor_chance: HealingFloorChance
|
||||
inactive_exp_gain: InactiveExpGain
|
||||
initial_floor: InitialFloor
|
||||
iris_floor_chance: IrisFloorChance
|
||||
iris_treasures_required: IrisTreasuresRequired
|
||||
|
||||
@@ -232,6 +232,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001B:0x28001B + 1] = self.o.inactive_exp_gain.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
|
||||
|
||||
@@ -309,6 +309,12 @@ org $8EFD2E ; unused region at the end of bank $8E
|
||||
DB $1E,$0B,$01,$2B,$05,$1A,$05,$00 ; add dekar
|
||||
DB $1E,$0B,$01,$2B,$04,$1A,$06,$00 ; add tia
|
||||
DB $1E,$0B,$01,$2B,$06,$1A,$07,$00 ; add lexis
|
||||
DB $1F,$0B,$01,$2C,$01,$1B,$02,$00 ; remove selan
|
||||
DB $1F,$0B,$01,$2C,$02,$1B,$03,$00 ; remove guy
|
||||
DB $1F,$0B,$01,$2C,$03,$1B,$04,$00 ; remove arty
|
||||
DB $1F,$0B,$01,$2C,$05,$1B,$05,$00 ; remove dekar
|
||||
DB $1F,$0B,$01,$2C,$04,$1B,$06,$00 ; remove tia
|
||||
DB $1F,$0B,$01,$2C,$06,$1B,$07,$00 ; remove lexis
|
||||
pullpc
|
||||
|
||||
SpecialItemUse:
|
||||
@@ -328,11 +334,15 @@ SpecialItemUse:
|
||||
SEP #$20
|
||||
LDA $8ED8C7,X ; load predefined bitmask with a single bit set
|
||||
BIT $077E ; check against EV flags $02 to $07 (party member flags)
|
||||
BNE + ; abort if character already present
|
||||
LDA $07A9 ; load EV register $11 (party counter)
|
||||
BEQ ++
|
||||
LDA.b #$30 ; character already present; modify pointer to point to L2SASM leave script
|
||||
ADC $09B7
|
||||
STA $09B7
|
||||
BRA +++
|
||||
++: LDA $07A9 ; character not present; load EV register $0B (party counter)
|
||||
CMP.b #$03
|
||||
BPL + ; abort if party full
|
||||
LDA.b #$8E
|
||||
+++ LDA.b #$8E
|
||||
STA $09B9
|
||||
PHK
|
||||
PEA ++
|
||||
@@ -340,7 +350,6 @@ SpecialItemUse:
|
||||
JML $83BB76 ; initialize parser variables
|
||||
++: NOP
|
||||
JSL $809CB8 ; call L2SASM parser
|
||||
JSL $81F034 ; consume the item
|
||||
TSX
|
||||
INX #13
|
||||
TXS
|
||||
@@ -490,6 +499,73 @@ pullpc
|
||||
|
||||
|
||||
|
||||
; allow inactive characters to gain exp
|
||||
pushpc
|
||||
org $81DADD
|
||||
; DB=$81, x=0, m=1
|
||||
NOP ; overwrites BNE $81DAE2 : JMP $DBED
|
||||
JML HandleActiveExp
|
||||
AwardExp:
|
||||
; isolate exp distribution into a subroutine, to be reused for both active party members and inactive characters
|
||||
org $81DAE9
|
||||
NOP #2 ; overwrites JMP $DBBD
|
||||
RTL
|
||||
org $81DB42
|
||||
NOP #2 ; overwrites JMP $DBBD
|
||||
RTL
|
||||
org $81DD11
|
||||
; DB=$81, x=0, m=1
|
||||
JSL HandleInactiveExp ; overwrites LDA $0A8A : CLC
|
||||
pullpc
|
||||
|
||||
HandleActiveExp:
|
||||
BNE + ; (overwritten instruction; modified) check if statblock not empty
|
||||
JML $81DBED ; (overwritten instruction; modified) abort
|
||||
+: JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
|
||||
JML $81DBBD ; (overwritten instruction; modified) continue to next level text
|
||||
|
||||
HandleInactiveExp:
|
||||
LDA $F0201B ; load inactive exp gain rate
|
||||
BEQ + ; zero gain; skip everything
|
||||
CMP.b #$64
|
||||
BCS ++ ; full gain
|
||||
LSR $1607
|
||||
ROR $1606 ; half gain
|
||||
ROR $1605
|
||||
++: LDY.w #$0000 ; start looping through all characters
|
||||
-: TDC
|
||||
TYA
|
||||
LDX.w #$0003 ; start looping through active party
|
||||
--: CMP $0A7B,X
|
||||
BEQ ++ ; skip if character in active party
|
||||
DEX
|
||||
BPL -- ; continue looping through active party
|
||||
STA $153D ; inactive character detected; overwrite character index of 1st slot in party battle order
|
||||
ASL
|
||||
TAX
|
||||
REP #$20
|
||||
LDA $859EBA,X ; convert character index to statblock pointer
|
||||
SEP #$20
|
||||
TAX
|
||||
PHY ; stash character loop index
|
||||
LDY $0A80
|
||||
PHY ; stash 1st (in menu order) party member statblock pointer
|
||||
STX $0A80 ; overwrite 1st (in menu order) party member statblock pointer
|
||||
LDY.w #$0000 ; set to use 1st position (in battle order)
|
||||
STY $00 ; set to use 1st position (in menu order)
|
||||
JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
|
||||
PLY ; restore 1st (in menu order) party member statblock pointer
|
||||
STY $0A80
|
||||
PLY ; restore character loop index
|
||||
++: INY
|
||||
CPY.w #$0007
|
||||
BCC - ; continue looping through all characters
|
||||
+: LDA $0A8A ; (overwritten instruction) load current gold
|
||||
CLC ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
|
||||
|
||||
; receive death link
|
||||
pushpc
|
||||
org $83BC91
|
||||
@@ -1226,6 +1302,7 @@ pullpc
|
||||
; $F02018 1 party members available
|
||||
; $F02019 1 capsule monsters available
|
||||
; $F0201A 1 shop interval
|
||||
; $F0201B 1 inactive exp gain rate
|
||||
; $F02030 1 selected goal
|
||||
; $F02031 1 goal completion: boss
|
||||
; $F02032 1 goal completion: iris_treasure_hunt
|
||||
|
||||
Binary file not shown.
@@ -53,8 +53,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
||||
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
|
||||
- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
|
||||
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
|
||||
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
|
||||
by using the character items from your inventory
|
||||
find them in order to unlock them for you to use. While cave diving, you can add or remove unlocked party members by
|
||||
using the character items from your inventory. There's also an option to allow inactive characters to gain some EXP,
|
||||
so that new party members added during a run don't have to start off at a low level
|
||||
|
||||
###### Quality of life:
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ from BaseClasses import Location
|
||||
class MeritousLocation(Location):
|
||||
game: str = "Meritous"
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||
super(MeritousLocation, self).__init__(player, name, address, parent)
|
||||
if "Wervyn Anixil" in name or "Defeat" in name:
|
||||
self.event = True
|
||||
|
||||
|
||||
offset = 593_000
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/se
|
||||
|
||||
### Où puis-je obtenir un fichier YAML ?
|
||||
|
||||
Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-settings)
|
||||
Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-options)
|
||||
|
||||
## Rejoindre une partie MultiWorld
|
||||
|
||||
@@ -71,4 +71,4 @@ les liens suivants sont les versions des logiciels que nous utilisons.
|
||||
- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
|
||||
- **NE PAS INSTALLER CECI SUR VOTRE CLIENT**
|
||||
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
|
||||
- choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
|
||||
- choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
|
||||
|
||||
@@ -103,8 +103,6 @@ shuffle_structures:
|
||||
off: 0
|
||||
```
|
||||
|
||||
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med
|
||||
Archipelago-installationen.
|
||||
|
||||
## Gå med i ett Multivärld-spel
|
||||
|
||||
|
||||
@@ -35,13 +35,14 @@ class MuseDashCollections:
|
||||
"Rush-Hour",
|
||||
"Find this Month's Featured Playlist",
|
||||
"PeroPero in the Universe",
|
||||
"umpopoff"
|
||||
"umpopoff",
|
||||
"P E R O P E R O Brother Dance",
|
||||
]
|
||||
|
||||
REMOVED_SONGS = [
|
||||
"CHAOS Glitch",
|
||||
"FM 17314 SUGAR RADIO",
|
||||
"Yume Ou Mono Yo Secret"
|
||||
"Yume Ou Mono Yo Secret",
|
||||
]
|
||||
|
||||
album_items: Dict[str, AlbumData] = {}
|
||||
@@ -57,6 +58,7 @@ class MuseDashCollections:
|
||||
"Chromatic Aberration Trap": STARTING_CODE + 5,
|
||||
"Background Freeze Trap": STARTING_CODE + 6,
|
||||
"Gray Scale Trap": STARTING_CODE + 7,
|
||||
"Focus Line Trap": STARTING_CODE + 10,
|
||||
}
|
||||
|
||||
sfx_trap_items: Dict[str, int] = {
|
||||
@@ -64,7 +66,19 @@ class MuseDashCollections:
|
||||
"Error SFX Trap": STARTING_CODE + 9,
|
||||
}
|
||||
|
||||
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
|
||||
filler_items: Dict[str, int] = {
|
||||
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
|
||||
"Miss To Great (5 Pack)": STARTING_CODE + 31,
|
||||
"Extra Life": STARTING_CODE + 32,
|
||||
}
|
||||
|
||||
filler_item_weights: Dict[str, int] = {
|
||||
"Great To Perfect (10 Pack)": 10,
|
||||
"Miss To Great (5 Pack)": 3,
|
||||
"Extra Life": 1,
|
||||
}
|
||||
|
||||
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
|
||||
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -518,7 +518,7 @@ Haunted Dance|43-48|MD Plus Project|False|6|9|11|
|
||||
Hey Vincent.|43-49|MD Plus Project|True|6|8|10|
|
||||
Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9|
|
||||
Narcissism Angel|43-51|MD Plus Project|True|1|3|6|
|
||||
AlterLuna|43-52|MD Plus Project|True|6|8|11|
|
||||
AlterLuna|43-52|MD Plus Project|True|6|8|11|12
|
||||
Niki Tousen|43-53|MD Plus Project|True|6|8|10|11
|
||||
Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9|
|
||||
Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10|
|
||||
@@ -537,4 +537,11 @@ Ruler Of My Heart VIVINOS|71-1|Valentine Stage|False|2|4|6|
|
||||
Reality Show|71-2|Valentine Stage|False|5|7|10|
|
||||
SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
|
||||
Rose Love|71-4|Valentine Stage|True|2|4|7|
|
||||
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
||||
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
||||
P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0|
|
||||
PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
|
||||
How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11
|
||||
Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12
|
||||
Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10|
|
||||
DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11|
|
||||
Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9|
|
||||
|
||||
@@ -4,11 +4,13 @@ from dataclasses import dataclass
|
||||
|
||||
from .MuseDashCollection import MuseDashCollections
|
||||
|
||||
|
||||
class AllowJustAsPlannedDLCSongs(Toggle):
|
||||
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
|
||||
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
|
||||
display_name = "Allow [Muse Plus] DLC Songs"
|
||||
|
||||
|
||||
class DLCMusicPacks(OptionSet):
|
||||
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
|
||||
display_name = "DLC Packs"
|
||||
@@ -101,20 +103,10 @@ class GradeNeeded(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class AdditionalItemPercentage(Range):
|
||||
"""The percentage of songs that will have 2 items instead of 1 when completing them.
|
||||
- Starting Songs will always have 2 items.
|
||||
- Locations will be filled with duplicate songs if there are not enough items.
|
||||
"""
|
||||
display_name = "Additional Item %"
|
||||
range_start = 50
|
||||
default = 80
|
||||
range_end = 100
|
||||
|
||||
|
||||
class MusicSheetCountPercentage(Range):
|
||||
"""Collecting enough Music Sheets will unlock the goal song needed for completion.
|
||||
This option controls how many are in the item pool, based on the total number of songs."""
|
||||
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
|
||||
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
|
||||
"""
|
||||
range_start = 10
|
||||
range_end = 40
|
||||
default = 20
|
||||
@@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
|
||||
streamer_mode_enabled: StreamerModeEnabled
|
||||
starting_song_count: StartingSongs
|
||||
additional_song_count: AdditionalSongs
|
||||
additional_item_percentage: AdditionalItemPercentage
|
||||
song_difficulty_mode: DifficultyMode
|
||||
song_difficulty_min: DifficultyModeOverrideMin
|
||||
song_difficulty_max: DifficultyModeOverrideMax
|
||||
|
||||
@@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": False,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
@@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
@@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 8,
|
||||
"additional_song_count": 91,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
|
||||
@@ -57,6 +57,8 @@ class MuseDashWorld(World):
|
||||
|
||||
# Necessary Data
|
||||
md_collection = MuseDashCollections()
|
||||
filler_item_names = list(md_collection.filler_item_weights.keys())
|
||||
filler_item_weights = list(md_collection.filler_item_weights.values())
|
||||
|
||||
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
|
||||
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
|
||||
@@ -70,7 +72,7 @@ class MuseDashWorld(World):
|
||||
|
||||
def generate_early(self):
|
||||
dlc_songs = {key for key in self.options.dlc_packs.value}
|
||||
if (self.options.allow_just_as_planned_dlc_songs.value):
|
||||
if self.options.allow_just_as_planned_dlc_songs.value:
|
||||
dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
|
||||
|
||||
streamer_mode = self.options.streamer_mode_enabled
|
||||
@@ -84,7 +86,7 @@ class MuseDashWorld(World):
|
||||
while True:
|
||||
# In most cases this should only need to run once
|
||||
available_song_keys = self.md_collection.get_songs_with_settings(
|
||||
dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold)
|
||||
dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
|
||||
|
||||
available_song_keys = self.handle_plando(available_song_keys)
|
||||
|
||||
@@ -161,19 +163,17 @@ class MuseDashWorld(World):
|
||||
break
|
||||
self.included_songs.append(available_song_keys.pop())
|
||||
|
||||
self.location_count = len(self.starting_songs) + len(self.included_songs)
|
||||
location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0)
|
||||
self.location_count = floor(self.location_count * location_multiplier)
|
||||
|
||||
minimum_location_count = len(self.included_songs) + self.get_music_sheet_count()
|
||||
if self.location_count < minimum_location_count:
|
||||
self.location_count = minimum_location_count
|
||||
self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs))
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
if name == self.md_collection.MUSIC_SHEET_NAME:
|
||||
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
|
||||
self.md_collection.MUSIC_SHEET_CODE, self.player)
|
||||
|
||||
filler = self.md_collection.filler_items.get(name)
|
||||
if filler:
|
||||
return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
|
||||
|
||||
trap = self.md_collection.vfx_trap_items.get(name)
|
||||
if trap:
|
||||
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
|
||||
@@ -189,6 +189,9 @@ class MuseDashWorld(World):
|
||||
song = self.md_collection.song_items.get(name)
|
||||
return MuseDashSongItem(name, self.player, song)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(self.filler_item_names, self.filler_item_weights)[0]
|
||||
|
||||
def create_items(self) -> None:
|
||||
song_keys_in_pool = self.included_songs.copy()
|
||||
|
||||
@@ -199,8 +202,13 @@ class MuseDashWorld(World):
|
||||
for _ in range(0, item_count):
|
||||
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
|
||||
|
||||
# Then add all traps
|
||||
trap_count = self.get_trap_count()
|
||||
# Then add 1 copy of every song
|
||||
item_count += len(self.included_songs)
|
||||
for song in self.included_songs:
|
||||
self.multiworld.itempool.append(self.create_item(song))
|
||||
|
||||
# Then add all traps, making sure we don't over fill
|
||||
trap_count = min(self.location_count - item_count, self.get_trap_count())
|
||||
trap_list = self.get_available_traps()
|
||||
if len(trap_list) > 0 and trap_count > 0:
|
||||
for _ in range(0, trap_count):
|
||||
@@ -209,23 +217,38 @@ class MuseDashWorld(World):
|
||||
|
||||
item_count += trap_count
|
||||
|
||||
# Next fill all remaining slots with song items
|
||||
needed_item_count = self.location_count
|
||||
while item_count < needed_item_count:
|
||||
# If we have more items needed than keys, just iterate the list and add them all
|
||||
if len(song_keys_in_pool) <= needed_item_count - item_count:
|
||||
for key in song_keys_in_pool:
|
||||
self.multiworld.itempool.append(self.create_item(key))
|
||||
# At this point, if a player is using traps, it's possible that they have filled all locations
|
||||
items_left = self.location_count - item_count
|
||||
if items_left <= 0:
|
||||
return
|
||||
|
||||
item_count += len(song_keys_in_pool)
|
||||
continue
|
||||
# When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
|
||||
# First fill 50% with the filler. The rest is to be duplicate songs.
|
||||
filler_count = floor(0.5 * items_left)
|
||||
items_left -= filler_count
|
||||
|
||||
# Otherwise add a random assortment of songs
|
||||
self.random.shuffle(song_keys_in_pool)
|
||||
for i in range(0, needed_item_count - item_count):
|
||||
self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
|
||||
for _ in range(0, filler_count):
|
||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
item_count = needed_item_count
|
||||
# All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression
|
||||
# to cut down on the number of progression items that Muse Dash puts into the pool.
|
||||
|
||||
# This is for the extraordinary case of needing to fill a lot of items.
|
||||
while items_left > len(song_keys_in_pool):
|
||||
for key in song_keys_in_pool:
|
||||
item = self.create_item(key)
|
||||
item.classification = ItemClassification.useful
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
items_left -= len(song_keys_in_pool)
|
||||
continue
|
||||
|
||||
# Otherwise add a random assortment of songs
|
||||
self.random.shuffle(song_keys_in_pool)
|
||||
for i in range(0, items_left):
|
||||
item = self.create_item(song_keys_in_pool[i])
|
||||
item.classification = ItemClassification.useful
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
@@ -245,8 +268,6 @@ class MuseDashWorld(World):
|
||||
self.random.shuffle(included_song_copy)
|
||||
all_selected_locations.extend(included_song_copy)
|
||||
|
||||
two_item_location_count = self.location_count - len(all_selected_locations)
|
||||
|
||||
# Make a region per song/album, then adds 1-2 item locations to them
|
||||
for i in range(0, len(all_selected_locations)):
|
||||
name = all_selected_locations[i]
|
||||
@@ -254,10 +275,11 @@ class MuseDashWorld(World):
|
||||
self.multiworld.regions.append(region)
|
||||
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
|
||||
|
||||
# Up to 2 Locations are defined per song
|
||||
region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation)
|
||||
if i < two_item_location_count:
|
||||
region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation)
|
||||
# Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
|
||||
region.add_locations({
|
||||
name + "-0": self.md_collection.song_locations[name + "-0"],
|
||||
name + "-1": self.md_collection.song_locations[name + "-1"]
|
||||
}, MuseDashLocation)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: \
|
||||
@@ -276,19 +298,14 @@ class MuseDashWorld(World):
|
||||
|
||||
return trap_list
|
||||
|
||||
def get_additional_item_percentage(self) -> int:
|
||||
trap_count = self.options.trap_count_percentage.value
|
||||
song_count = self.options.music_sheet_count_percentage.value
|
||||
return max(trap_count + song_count, self.options.additional_item_percentage.value)
|
||||
|
||||
def get_trap_count(self) -> int:
|
||||
multiplier = self.options.trap_count_percentage.value / 100.0
|
||||
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
||||
trap_count = len(self.starting_songs) + len(self.included_songs)
|
||||
return max(0, floor(trap_count * multiplier))
|
||||
|
||||
def get_music_sheet_count(self) -> int:
|
||||
multiplier = self.options.music_sheet_count_percentage.value / 100.0
|
||||
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
||||
song_count = len(self.starting_songs) + len(self.included_songs)
|
||||
return max(1, floor(song_count * multiplier))
|
||||
|
||||
def get_music_sheet_win_count(self) -> int:
|
||||
@@ -329,5 +346,4 @@ class MuseDashWorld(World):
|
||||
"deathLink": self.options.death_link.value,
|
||||
"musicSheetWinCount": self.get_music_sheet_win_count(),
|
||||
"gradeNeeded": self.options.grade_needed.value,
|
||||
"hasFiller": True,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Enlaces rápidos
|
||||
- [Página Principal](../../../../games/Muse%20Dash/info/en)
|
||||
- [Página de Configuraciones](../../../../games/Muse%20Dash/player-settings)
|
||||
- [Página de Configuraciones](../../../../games/Muse%20Dash/player-options)
|
||||
|
||||
## Software Requerido
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago.
|
||||
|
||||
## Generar un juego MultiWorld
|
||||
1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-settings) y configura las opciones del juego a tu gusto.
|
||||
1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-options) y configura las opciones del juego a tu gusto.
|
||||
2. Genera tu archivo YAML y úsalo para generar un juego nuevo en el radomizer
|
||||
- (Instrucciones sobre como generar un juego en Archipelago disponibles en la [guía web de Archipelago en Inglés](/tutorial/Archipelago/setup/en))
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
def test_all_difficulty_ranges(self) -> None:
|
||||
muse_dash_world = self.multiworld.worlds[1]
|
||||
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
|
||||
difficulty_choice = self.multiworld.song_difficulty_mode[1]
|
||||
difficulty_min = self.multiworld.song_difficulty_min[1]
|
||||
difficulty_max = self.multiworld.song_difficulty_max[1]
|
||||
difficulty_choice = muse_dash_world.options.song_difficulty_mode
|
||||
difficulty_min = muse_dash_world.options.song_difficulty_min
|
||||
difficulty_max = muse_dash_world.options.song_difficulty_max
|
||||
|
||||
def test_range(inputRange, lower, upper):
|
||||
self.assertEqual(inputRange[0], lower)
|
||||
@@ -66,9 +66,9 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
|
||||
song = muse_dash_world.md_collection.song_items[song_name]
|
||||
|
||||
# umpopoff is a one time weird song. Its currently the only song in the game
|
||||
# with non-standard difficulties and also doesn't have 3 or more difficulties.
|
||||
if song_name == 'umpopoff':
|
||||
# Some songs are weird and have less than the usual 3 difficulties.
|
||||
# So this override is to avoid failing on these songs.
|
||||
if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
|
||||
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
|
||||
f"Song '{song_name}' difficulty not set when it should be.")
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MuseDashTestBase(WorldTestBase):
|
||||
|
||||
@@ -38,7 +38,7 @@ class NoitaWorld(World):
|
||||
|
||||
web = NoitaWeb()
|
||||
|
||||
def generate_early(self):
|
||||
def generate_early(self) -> None:
|
||||
if not self.multiworld.get_player_name(self.player).isascii():
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class NoitaLocation(Location):
|
||||
class LocationData(NamedTuple):
|
||||
id: int
|
||||
flag: int = 0
|
||||
ltype: Optional[str] = "shop"
|
||||
ltype: str = "shop"
|
||||
|
||||
|
||||
class LocationFlag(IntEnum):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user