Compare commits

...

64 Commits

Author SHA1 Message Date
Fabian Dill
7ae486ad04 Update BaseClasses.py
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2024-04-18 18:35:46 +02:00
Fabian Dill
25a5e37cc6 Core: switch to singleton pattern 2024-04-17 08:20:57 +02:00
Fabian Dill
860df6e658 Core: clean up observer thread 2024-04-17 03:37:43 +02:00
Fabian Dill
16525c91b8 Core: observe and report currently long-running function 2024-04-17 02:45:00 +02:00
Doug Hoskisson
30cdde8605 CI: pyright in github actions (#3121)
* CI: strict mypy check in github actions

mypy_files.txt is a list of files that will fail the CI if mypy finds errors in them

* don't need these

* `Any` should be a way to silence the type checker

* restrict return Any

* CI: pyright in github actions

* fix mistake in translating from mypy

* missed another change from mypy to pyright

* pin pyright version

* add more paths that should trigger check

* use Python instead of bash

* type error for testing CI

* Revert "type error for testing CI"

This reverts commit 99f65f3dad.

* oops

* don't need to redirect output
2024-04-16 23:03:30 +02:00
Fabian Dill
38c54ba393 WebHost: check: display exception chain one layer deep (#3153)
* WebHost: check: display exception chain one layer deep

* Update WebHostLib/check.py
2024-04-15 21:26:59 -04:00
Ziktofel
5da3a40964 SC2 Documentation: Fix the page titles (#3074) 2024-04-16 02:55:36 +02:00
Trevor L
a1ef25455b Hylics 2: Fix logic for medallions in vault (#3148) 2024-04-16 02:54:35 +02:00
Fabian Dill
feb62b4af2 Core: increment version (#3144) 2024-04-16 02:53:12 +02:00
Scipio Wright
6dbeb6c658 TUNIC: Fix chest in incorrect region, incorrect key requirement (#3132) 2024-04-14 21:14:16 -05:00
Fabian Dill
09abc5beaa Core: add visibility attribute to Option (#3125) 2024-04-14 20:49:43 +02:00
Fabian Dill
fb1cf26118 SNIClient/LttP: modern SNI prevents payload overflow (#2523) 2024-04-14 20:40:09 +02:00
Aaron Wagener
842a15fd3c Core: replace Location.event with advancement property (#2871) 2024-04-14 20:37:48 +02:00
Fabian Dill
f67e8497e0 kvui: use all flags in Item Class tooltip (#3011) 2024-04-14 20:36:55 +02:00
Fabian Dill
19f1b265b1 LttP: deprioritize locked locations for ingame hints (#3127) 2024-04-14 20:36:36 +02:00
Fabian Dill
1c14d1107f Subnautica: filler items distribution (#3104) 2024-04-14 20:36:25 +02:00
Fabian Dill
7b3727e945 CommonClient: set max_size to 16 MB (#3124) 2024-04-14 20:36:08 +02:00
Justus Lind
d1274c12b9 Muse Dash: Add filler items and rework generation balance (#2809) 2024-04-14 20:23:13 +02:00
NewSoupVi
7c44d749d4 MultiServer: Support location name groups in !missing and !checked commands (#2538) 2024-04-14 20:22:12 +02:00
Silvris
f1765899c4 MultiServer: add all worlds goal completion message (#2956) 2024-04-14 20:16:45 +02:00
PoryGone
87b9f4a6fa Spoiler: Display all precollected items in the Spoiler Log (#2928) 2024-04-14 20:05:16 +02:00
Scipio Wright
480c15eea0 TUNIC: Fix entrance rule for unrestricted + ladders - entrance rando (#3076) 2024-04-14 19:59:34 +02:00
Alchav
d4ec4d32f0 ALTTP: Bomb Walls Logic Fixes (#3130) 2024-04-14 17:30:40 +02:00
Bryce Wilson
50fb70d832 BizHawkClient: Add error message if patching fails (#2877) 2024-04-14 03:26:25 +02:00
zig-for
ca5c0d9eb8 LADX: Add "boots controls" option (#2085) 2024-04-14 03:21:55 +02:00
Aaron Wagener
98e2d89a1c Core: Let location name groups work with /hint_location (#2814) 2024-04-14 02:25:27 +02:00
PinkSwitch
5bda265f43 Yoshi's Island: Fix Outdated Connection Setup (#3113) 2024-04-14 02:23:59 +02:00
NewSoupVi
9ef1fa825d The Witness: Rename "Town Windmill Entry" to "Windmill Entry" (#3081) 2024-04-14 02:21:18 +02:00
Seldom
f5ff005360 Terraria: Crate logic (#2841) 2024-04-14 02:18:02 +02:00
Scipio Wright
fb3035a78b TUNIC: Some cleanup (#3115) 2024-04-14 02:06:06 +02:00
Ziktofel
3d5c21cec5 SC2: Remove the deprecated data version attribute in order to avoid cached old item names (#3040) 2024-04-14 01:44:51 +02:00
Scipio Wright
feaae1db12 Noita: Do some cleanup to make mypy happy (#3114) 2024-04-14 01:21:20 +02:00
Phaneros
56ec0e902d sc2: Fixing mission levels not counting towards the level 35 threshold to unlock primal kerrigan (#3109) 2024-04-14 01:09:01 +02:00
agilbert1412
c8fd42f938 Stardew Valley: Remove early shipping bin documentation (#3126) 2024-04-14 00:46:41 +02:00
Star Rauchenberger
e5eb54fb27 The Witness: Migrate joke hints to the client (#3049) 2024-04-14 00:46:11 +02:00
Scipio Wright
fbeba1e470 TUNIC: Error catching for logic bugs in ER (#3082) 2024-04-14 00:20:52 +02:00
Star Rauchenberger
11073dfdac Lingo: Remove unnecessary player_logic parameters (#3054)
A world's player_logic is accessible from the LingoWorld object, so it's not necessary to also pass the LingoPlayerLogic object through every function that uses both.
2024-04-14 00:20:31 +02:00
Alchav
7e660dbd23 Pokémon Red and Blue: 0.4.5 Fixes (#3106) 2024-04-13 17:58:50 +02:00
Exempt-Medic
1fc2c5ed4b Core: Getting rid of forfeit_mode (#3099) 2024-04-12 21:25:33 +02:00
Chris Wilson
242126b4b2 ArchipIDLE 2024 (#3079)
* Update item pool to include 25 jokes and videos as progression items, as well as a progression GeroCities profile

* Fix a bug in Items.py causing item names to be appended inappropriately

* Remove unnecessary import

* Change item pool to have 50 jokes and 20 motivational videos

* Adjust item pool to have 40 of both jokes and videos

* Fix imports to allow compressing for distribution as a .apworld
2024-04-12 00:32:10 -04:00
Ziktofel
30dda013de SC2: Fix Typos in location names (#3108) 2024-04-12 03:01:12 +02:00
Scipio Wright
ea4d0abb7f Webhost: Fix a typo on Start Playing page (#3122)
* add an or

* Changed the wording to account for uploading multiple files
2024-04-11 19:31:42 -04:00
PoryGone
9bbc49204d DKC3: Fix List Out of Range Error on Level Shuffle Hint extension (#3077) 2024-04-12 00:53:52 +02:00
Ziktofel
b97cee4372 SC2: Fix vanilla mission order connection (#3101) 2024-04-12 00:52:27 +02:00
Aaron Wagener
5dcafac861 Core: add Location.is_event property (#2968) 2024-04-12 00:49:22 +02:00
Ziktofel
8d28c34f95 SC2: Fix unused_items refill to respect item dependencies. (#3116) 2024-04-12 00:46:15 +02:00
Justus Lind
0f2bd0fb85 Muse Dash: Update songs to 4.2.0. Add a new trap. (#3053) 2024-04-12 00:44:16 +02:00
Bryce Wilson
cf59cfaad0 Pokemon Emerald: Change Ho-Oh capitalization (#3069) 2024-04-12 00:31:53 +02:00
Scipio Wright
c534cb79b5 TUNIC: Fix link to player options page in setup guide (#3086) 2024-04-12 00:30:31 +02:00
Silvris
8952fbdc03 KDL3: Fix boss access on open world disabled (#3120) 2024-04-12 00:29:40 +02:00
Ziktofel
9012afeb75 SC2: Fix possible non-determinism in goal selection (#3123) 2024-04-12 00:28:59 +02:00
NewSoupVi
401a6d9a42 The Witness: The big dumb refactor (#3007) 2024-04-12 00:27:42 +02:00
Aaron Wagener
5d4ed00452 Webhost: add file downloads to the room api endpoint (#2780) 2024-04-11 05:05:52 +02:00
Exempt-Medic
0ba6d90bb8 Fix typo (#3094) 2024-04-10 00:05:02 -04:00
Rjosephson
b007a42487 Ror2: Add progressive stages option (#2813) 2024-04-09 21:14:18 +02:00
qwint
32c92e03e7 Hollow Knight: Adding Godhome Goal Logic (#2952) 2024-04-09 21:12:50 +02:00
el-u
14437d653f lufia2ac: ability to swap party members mid-run and option to gain EXP while inactive (#2800) 2024-04-09 00:33:34 +02:00
Fabian Dill
1021df8b1b Core: remove now unused stuff in Generate.py (#3035) 2024-04-09 00:24:38 +02:00
Nicholas Saylor
569c37cb8e Core, Webhost, Docs: Replace all usages of player settings (#3067)
* Replace all usages of player settings

* Fixed line break error

* Attempt to fix line break again

* Finally figure out what Pycharm did to this file

* Pycharm search failed me

* Remove duplicate s

* Update ArchipIdle

* Revert random newline changes from Pycharm

* Remove player settings from fstrings and rename --samesettings to --sameoptions

* Finally get PyCharm to not auto-format my commits, randomly inserting the newlines

* Removing player-settings

* Missed one

* Remove final line break error
Co-authored-by: Exempt-Medic <60412657+exempt-medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
2024-04-06 19:25:26 -04:00
Robyn (Reckoner)
b296f64d3c fixing minor typos on options (#3080) 2024-04-06 19:17:48 -04:00
Alchav
8d9bd0135e LTTP: Fix Bug With Custom Resource Spending (#3105) 2024-04-06 19:53:20 +02:00
Fabian Dill
885fb4aabe LttP: fix fix fake world always applying 2024-04-06 17:40:52 +02:00
Fabian Dill
3c564d7b96 WebHost: allow deleting Rooms and Seeds, as well as their associated data (#3071) 2024-04-02 16:45:07 +02:00
Alchav
5e5792009c LttP: delete playerSettings.yaml (#3062) 2024-04-01 19:08:21 +02:00
215 changed files with 2513 additions and 2595 deletions

27
.github/pyright-config.json vendored Normal file
View 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
View 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
View 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

View File

@@ -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')

View File

@@ -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
View File

@@ -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:

View File

@@ -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]"""

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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")

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"""

View File

@@ -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):

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

@@ -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.

View File

@@ -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

View File

@@ -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)'),

View File

@@ -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'],

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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'],

View File

@@ -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) + '.'

View File

@@ -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':

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)']],
])

View File

@@ -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']],
])

View File

@@ -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',

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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'),

View File

@@ -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.

View File

@@ -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"

View File

@@ -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]

View File

@@ -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]]

View File

@@ -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.

View File

@@ -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.

View File

@@ -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))

View File

@@ -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.")

View File

@@ -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

View File

@@ -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
View 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)

View File

@@ -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():

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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))

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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
"""))

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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|

View File

@@ -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

View File

@@ -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,
},

View File

@@ -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,
}

View File

@@ -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))

View File

@@ -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:

View File

@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
class MuseDashTestBase(WorldTestBase):

View File

@@ -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).")

View File

@@ -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