Compare commits

..

58 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
187 changed files with 2400 additions and 2021 deletions
+27
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
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
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
+63 -16
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')
+3 -1
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)
+3 -7
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:
-8
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
@@ -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]"""
+1 -1
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
+25 -7
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:
+26 -1
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()
+6 -10
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
+1 -1
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")
+21 -2
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,
}
+4 -1
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
+22 -5
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"
-3
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>
+1 -1
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 />
-5
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`.
+10 -8
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):
+1 -1
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)
+5 -7
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"""
+1 -1
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):
+6 -2
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:
+1 -1
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):
+5 -2
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:
+8
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)'),
+5 -3
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'],
+1 -3
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
+8 -4
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,
}
+4 -2
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'],
+9 -6
View File
@@ -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) + '.'
+10 -2
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':
+13
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)",
@@ -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)']],
])
+4 -4
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']],
])
+5 -2
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',
+10 -20
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)
+35 -14
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
-4
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'),
+1 -1
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"
+6 -1
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]
+1 -1
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]]
+2 -2
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))
+1 -1
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.")
+55
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)
+4
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():
+2 -2
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
+2
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):
+10
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)
+5
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))
+5 -1
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:
+1 -1
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
+5 -2
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
-1
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):
@@ -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
+97 -5
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
"""))
+4 -6
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
+16 -1
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,
}
+1 -1
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
+1 -1
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]
+17 -21
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()
+24 -28
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)
+16 -1
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
+1
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()
+81 -4
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
Binary file not shown.
@@ -53,8 +53,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
by using the character items from your inventory
find them in order to unlock them for you to use. While cave diving, you can add or remove unlocked party members by
using the character items from your inventory. There's also an option to allow inactive characters to gain some EXP,
so that new party members added during a run don't have to start off at a low level
###### Quality of life:
-5
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
+17 -3
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:
+9 -2
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|
+5 -14
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
-3
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,
},
+55 -39
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,
}
+6 -6
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:
+1 -1
View File
@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
class MuseDashTestBase(WorldTestBase):
+1 -1
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).")
+1 -1
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):
+1 -1
View File
@@ -41,7 +41,7 @@ def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
# An "Entrance" is really just a connection between two regions
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]):
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance:
entrance = Entrance(player, f"From {source} To {destination}", regions[source])
entrance.connect(regions[destination])
return entrance
+2 -2
View File
@@ -68,7 +68,7 @@ def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]) -> None:
for shop_location in shop_locations:
location = world.multiworld.get_location(shop_location, world.player)
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
@@ -129,7 +129,7 @@ def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
)
def biome_unlock_conditions(world: "NoitaWorld"):
def biome_unlock_conditions(world: "NoitaWorld") -> None:
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
+1 -4
View File
@@ -44,14 +44,11 @@ class OOTLocation(Location):
self.vanilla_item = vanilla_item
if filter_tags is None:
self.filter_tags = None
else:
else:
self.filter_tags = list(filter_tags)
self.never = False # no idea what this does
self.disabled = DisableType.ENABLED
if type == 'Event':
self.event = True
@property
def dungeon(self):
return self.parent_region.dungeon
+1 -3
View File
@@ -717,7 +717,6 @@ class OOTWorld(World):
item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False)
location.locked = True
location.event = True
if name not in item_table:
location.internal = True
return item
@@ -842,7 +841,7 @@ class OOTWorld(World):
all_state.sweep_for_events(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
(loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
for loc in unreachable:
loc.parent_region.locations.remove(loc)
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
@@ -972,7 +971,6 @@ class OOTWorld(World):
for location in song_locations:
location.item = None
location.locked = False
location.event = False
else:
break
-2
View File
@@ -115,8 +115,6 @@ class Overcooked2World(World):
region,
)
location.event = is_event
if priority:
location.progress_type = LocationProgressType.PRIORITY
else:
+1 -1
View File
@@ -98,7 +98,7 @@ LEGENDARY_NAMES = {
"Registeel": "REGISTEEL",
"Mew": "MEW",
"Deoxys": "DEOXYS",
"Ho-oh": "HO_OH",
"Ho-Oh": "HO_OH",
"Lugia": "LUGIA",
}
+1 -1
View File
@@ -741,7 +741,7 @@ def _init() -> None:
("SPECIES_PUPITAR", "Pupitar", 247),
("SPECIES_TYRANITAR", "Tyranitar", 248),
("SPECIES_LUGIA", "Lugia", 249),
("SPECIES_HO_OH", "Ho-oh", 250),
("SPECIES_HO_OH", "Ho-Oh", 250),
("SPECIES_CELEBI", "Celebi", 251),
("SPECIES_TREECKO", "Treecko", 252),
("SPECIES_GROVYLE", "Grovyle", 253),
+1 -1
View File
@@ -2877,7 +2877,7 @@
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_250": {
"label": "Pokedex - Ho-oh",
"label": "Pokedex - Ho-Oh",
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_251": {
+2 -2
View File
@@ -246,7 +246,7 @@ class AllowedLegendaryHuntEncounters(OptionSet):
"Regirock"
"Registeel"
"Regice"
"Ho-oh"
"Ho-Oh"
"Lugia"
"Deoxys"
"Mew"
@@ -261,7 +261,7 @@ class AllowedLegendaryHuntEncounters(OptionSet):
"Regirock",
"Registeel",
"Regice",
"Ho-oh",
"Ho-Oh",
"Lugia",
"Deoxys",
"Mew",
+1 -1
View File
@@ -56,7 +56,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
"Registeel": "REGISTEEL",
"Mew": "MEW",
"Deoxys": "DEOXYS",
"Ho-oh": "HO_OH",
"Ho-Oh": "HO_OH",
"Lugia": "LUGIA",
}.items()
if name in world.options.allowed_legendary_hunt_encounters.value
+13 -90
View File
@@ -18,7 +18,7 @@ from .options import pokemon_rb_options
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
from .pokemon import process_pokemon_data, process_move_data
from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
from .encounters import process_pokemon_locations, process_trainer_data
from .rules import set_rules
from .level_scaling import level_scaling
@@ -265,7 +265,6 @@ class PokemonRedBlueWorld(World):
state = sweep_from_pool(multiworld.state, progitempool + unplaced_items)
if (not item.advancement) or state.can_reach(loc, "Location", loc.player):
multiworld.push_item(loc, item, False)
loc.event = item.advancement
fill_locations.remove(loc)
break
else:
@@ -279,12 +278,12 @@ class PokemonRedBlueWorld(World):
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
if not self.multiworld.badgesanity[self.player]:
# Door Shuffle options besides Simple place badges during door shuffling
if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"):
if self.multiworld.door_shuffle[self.player] in ("off", "simple"):
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
for badge in badges:
self.multiworld.itempool.remove(badge)
progitempool.remove(badge)
for _ in range(5):
for attempt in range(6):
badgelocs = [
self.multiworld.get_location(loc, self.player) for loc in [
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
@@ -293,6 +292,12 @@ class PokemonRedBlueWorld(World):
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
] if self.multiworld.get_location(loc, self.player).item is None]
state = self.multiworld.get_all_state(False)
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
if attempt > 1:
for mon in poke_data.pokemon_data.keys():
state.collect(self.create_item(mon), True)
state.sweep_for_events()
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
@@ -312,6 +317,7 @@ class PokemonRedBlueWorld(World):
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
verify_hm_moves(self.multiworld, self, self.player)
if self.multiworld.key_items_only[self.player]:
return
@@ -355,97 +361,14 @@ class PokemonRedBlueWorld(World):
for location in self.multiworld.get_locations(self.player):
if location.name in locs:
location.show_in_spoiler = False
def intervene(move, test_state):
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
def number_of_zones(mon):
zones = set()
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
zones.add(loc.name.split(" - ")[0])
return len(zones)
placed_mons = [slot.item.name for slot in accessible_slots]
if self.multiworld.area_1_to_1_mapping[self.player]:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = self.multiworld.random.choice(viable_mons)
replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if self.multiworld.area_1_to_1_mapping[self.player]:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
for replace_slot in replace_slots:
replace_slot.item = self.create_item(replace_mon)
else:
replace_slot.item = self.create_item(replace_mon)
else:
tms_hms = self.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)]
self.multiworld.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in
[self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]])
for mon in mon_list:
if test_state.has(mon, self.player):
self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
break
last_intervene = None
while True:
intervene_move = None
test_state = self.multiworld.get_all_state(False)
if not logic.can_learn_hm(test_state, "Surf", self.player):
intervene_move = "Surf"
elif not logic.can_learn_hm(test_state, "Strength", self.player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and
(self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(
self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player],
self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", self.player))
and self.multiworld.dark_rock_tunnel_logic[self.player]
and (self.multiworld.accessibility[self.player] != "minimal"
or self.multiworld.door_shuffle[self.player])):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", self.player))
and self.multiworld.door_shuffle[self.player] not in
("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
if intervene_move == last_intervene:
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}")
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
verify_hm_moves(self.multiworld, self, self.player)
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
all_state = self.multiworld.get_all_state(False)
evolutions_region = self.multiworld.get_region("Evolution", self.player)
for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player):
if not all_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
if self.multiworld.old_man[self.player] == "early_parcel":
+2 -2
View File
@@ -31,7 +31,7 @@ DATA_LOCATIONS = {
"CrashCheck2": (0x1617, 1),
# Progressive keys, should never be above 10. Just before Dexsanity flags.
"CrashCheck3": (0x1A70, 1),
# Route 18 script value. Should never be above 2. Just before Hidden items flags.
# Route 18 Gate script value. Should never be above 3. Just before Hidden items flags.
"CrashCheck4": (0x16DD, 1),
}
@@ -116,7 +116,7 @@ class PokemonRBClient(BizHawkClient):
or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF
or data["CrashCheck2"][0]
or data["CrashCheck3"][0] > 10
or data["CrashCheck4"][0] > 2):
or data["CrashCheck4"][0] > 3):
# Should mean game crashed
logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.")
self.game_state = False
-2
View File
@@ -197,7 +197,6 @@ def process_pokemon_locations(self):
mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random)
placed_mons[mon] += 1
location.item = self.create_item(mon)
location.event = True
location.locked = True
location.item.location = location
locations.append(location)
@@ -269,7 +268,6 @@ def process_pokemon_locations(self):
for slot in encounter_slots:
location = self.multiworld.get_location(slot.name, self.player)
location.item = self.create_item(slot.original_item)
location.event = True
location.locked = True
location.item.location = location
placed_mons[location.item.name] += 1
+1 -1
View File
@@ -175,7 +175,7 @@ location_data = [
LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"],
Missable(25)),
LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)),
LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)),
LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)),
LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)),
+94 -7
View File
@@ -1,5 +1,5 @@
from copy import deepcopy
from . import poke_data
from . import poke_data, logic
from .rom_addresses import rom_addresses
@@ -135,7 +135,6 @@ def process_pokemon_data(self):
learnsets = deepcopy(poke_data.learnsets)
tms_hms = self.local_tms + poke_data.hm_moves
compat_hms = set()
for mon, mon_data in local_poke_data.items():
@@ -323,19 +322,20 @@ def process_pokemon_data(self):
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
hm_verify = ["Surf", "Strength"]
if self.multiworld.accessibility[self.player] == "locations" or ((not
if self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")):
hm_verify += ["Cut"]
if self.multiworld.accessibility[self.player] == "locations" or (not
if self.multiworld.accessibility[self.player] != "minimal" or (not
self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or
self.multiworld.extra_key_items[self.player])
or self.multiworld.door_shuffle[self.player]):
hm_verify += ["Flash"]
# Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions,
# so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can
# learn it this simply would not occur
# Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable
# regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for
# door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would
# ensure connections to those towns.
for hm_move in hm_verify:
if hm_move not in compat_hms:
@@ -346,3 +346,90 @@ def process_pokemon_data(self):
self.local_poke_data = local_poke_data
self.learnsets = learnsets
def verify_hm_moves(multiworld, world, player):
def intervene(move, test_state):
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit]
if multiworld.randomize_wild_pokemon[player] and viable_mons:
accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if
loc.type == "Wild Encounter"]
def number_of_zones(mon):
zones = set()
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
zones.add(loc.name.split(" - ")[0])
return len(zones)
placed_mons = [slot.item.name for slot in accessible_slots]
if multiworld.area_1_to_1_mapping[player]:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = multiworld.random.choice(viable_mons)
replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if multiworld.area_1_to_1_mapping[player]:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
for replace_slot in replace_slots:
replace_slot.item = world.create_item(replace_mon)
else:
replace_slot.item = world.create_item(replace_mon)
else:
tms_hms = world.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)]
multiworld.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in
[world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]])
for mon in mon_list:
if test_state.has(mon, player):
world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
break
last_intervene = None
while True:
intervene_move = None
test_state = multiworld.get_all_state(False)
if not logic.can_learn_hm(test_state, "Surf", player):
intervene_move = "Surf"
elif not logic.can_learn_hm(test_state, "Strength", player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
elif ((not logic.can_learn_hm(test_state, "Cut", player)) and
(multiworld.accessibility[player] != "minimal" or ((not
multiworld.badgesanity[player]) and max(
multiworld.elite_four_badges_condition[player],
multiworld.route_22_gate_condition[player],
multiworld.victory_road_condition[player])
> 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", player))
and multiworld.dark_rock_tunnel_logic[player]
and (multiworld.accessibility[player] != "minimal"
or multiworld.door_shuffle[player])):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", player))
and multiworld.door_shuffle[player] not in
("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
if intervene_move == last_intervene:
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}")
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
+1 -2
View File
@@ -1540,7 +1540,6 @@ def create_regions(self):
item = self.create_filler()
elif location.original_item == "Pokedex":
if self.multiworld.randomize_pokedex[self.player] == "vanilla":
location_object.event = True
event = True
item = self.create_item("Pokedex")
elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]:
@@ -1948,7 +1947,7 @@ def create_regions(self):
for entrance in reversed(region.exits):
if isinstance(entrance, PokemonRBWarp):
region.exits.remove(entrance)
multiworld.regions.entrance_cache[self.player] = cache
multiworld.regions.entrance_cache[self.player] = cache.copy()
if badge_locs:
for loc in badge_locs:
loc.item = None
+21 -6
View File
@@ -44,8 +44,8 @@ class RiskOfRainWorld(World):
}
location_name_to_id = item_pickups
data_version = 8
required_client_version = (0, 4, 4)
data_version = 9
required_client_version = (0, 4, 5)
web = RiskOfWeb()
total_revivals: int
@@ -91,6 +91,17 @@ class RiskOfRainWorld(World):
# only mess with the environments if they are set as items
if self.options.goal == "explore":
# check to see if the user doesn't want to use stages, and to figure out what type of stages are being used.
if not self.options.require_stages:
if not self.options.progressive_stages:
self.multiworld.push_precollected(self.multiworld.create_item("Stage 1", self.player))
self.multiworld.push_precollected(self.multiworld.create_item("Stage 2", self.player))
self.multiworld.push_precollected(self.multiworld.create_item("Stage 3", self.player))
self.multiworld.push_precollected(self.multiworld.create_item("Stage 4", self.player))
else:
for _ in range(4):
self.multiworld.push_precollected(self.multiworld.create_item("Progressive Stage", self.player))
# figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table
if self.options.dlc_sotv:
@@ -121,8 +132,12 @@ class RiskOfRainWorld(World):
total_locations = self.options.total_locations.value
else:
# explore mode
# Add Stage items for logic gates
itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"]
# Add Stage items to the pool
if self.options.require_stages:
itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"] if not self.options.progressive_stages else \
["Progressive Stage"] * 4
total_locations = len(
get_locations(
chests=self.options.chests_per_stage.value,
@@ -206,8 +221,8 @@ class RiskOfRainWorld(World):
options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations",
"chests_per_stage", "shrines_per_stage", "scavengers_per_stage",
"scanner_per_stage", "altars_per_stage", "total_revivals",
"start_with_revive", "final_stage_death", "death_link",
casing="camel")
"start_with_revive", "final_stage_death", "death_link", "require_stages",
"progressive_stages", casing="camel")
return {
**options_dict,
"seed": "".join(self.random.choice(string.digits) for _ in range(16)),
-1
View File
@@ -57,7 +57,6 @@ options apply, so each Risk of Rain 2 player slot in the multiworld needs to be
for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op
instance can't make progress towards multiple player slots in the multiworld.
Explore mode is untested in multiplayer and will likely not work until a later release.
## What Risk of Rain items can appear in other players' worlds?
+1 -1
View File
@@ -59,7 +59,7 @@ stage_table: Dict[str, RiskOfRainItemData] = {
"Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression),
"Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression),
"Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression),
"Progressive Stage": RiskOfRainItemData("Stage", 5 + stage_offset, ItemClassification.progression),
}
item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table}
+13
View File
@@ -151,6 +151,17 @@ class DLC_SOTV(Toggle):
display_name = "Enable DLC - SOTV"
class RequireStages(DefaultOnToggle):
"""Add Stage items to the pool to block access to the next set of environments."""
display_name = "Require Stages"
class ProgressiveStages(DefaultOnToggle):
"""This will convert Stage items to be a progressive item. For example instead of "Stage 2" it would be
"Progressive Stage" """
display_name = "Progressive Stages"
class GreenScrap(Range):
"""Weight of Green Scraps in the item pool.
@@ -378,6 +389,8 @@ class ROR2Options(PerGameCommonOptions):
start_with_revive: StartWithRevive
final_stage_death: FinalStageDeath
dlc_sotv: DLC_SOTV
require_stages: RequireStages
progressive_stages: ProgressiveStages
death_link: DeathLink
item_pickup_step: ItemPickupStep
shrine_use_step: ShrineUseStep
+10 -13
View File
@@ -15,6 +15,13 @@ def has_entrance_access_rule(multiworld: MultiWorld, stage: str, region: str, pl
entrance.access_rule = rule
def has_stage_access_rule(multiworld: MultiWorld, stage: str, amount: int, region: str, player: int) -> None:
rule = lambda state: state.has(region, player) and \
(state.has(stage, player) or state.count("Progressive Stage", player) >= amount)
for entrance in multiworld.get_region(region, player).entrances:
entrance.access_rule = rule
def has_all_items(multiworld: MultiWorld, items: Set[str], region: str, player: int) -> None:
rule = lambda state: state.has_all(items, player) and state.has(region, player)
for entrance in multiworld.get_region(region, player).entrances:
@@ -43,15 +50,6 @@ def check_location(state, environment: str, player: int, item_number: int, item_
return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player)
# unlock event to next set of stages
def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int) -> None:
if stage_number == 4:
return
rule = lambda state: state.has(f"Stage {stage_number + 1}", player)
for entrance in multiworld.get_region(f"OrderedStage_{stage_number + 1}", player).entrances:
entrance.access_rule = rule
def set_rules(ror2_world: "RiskOfRainWorld") -> None:
player = ror2_world.player
multiworld = ror2_world.multiworld
@@ -124,8 +122,7 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None:
for newt in range(1, newts + 1):
has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
if i > 0:
has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player)
get_stage_event(multiworld, player, i)
has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
if ror2_options.dlc_sotv:
for i in range(len(environment_sotv_orderedstages_table)):
@@ -143,10 +140,10 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None:
for newt in range(1, newts + 1):
has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar")
if i > 0:
has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player)
has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player)
has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole",
player)
has_entrance_access_rule(multiworld, "Stage 1", "Hidden Realm: Bazaar Between Time", player)
has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player)
has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player)
has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player)
has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player)
+3 -1
View File
@@ -3,7 +3,9 @@ from . import RoR2TestBase
class MithrixGoalTest(RoR2TestBase):
options = {
"victory": "mithrix"
"victory": "mithrix",
"require_stages": "true",
"progressive_stages": "false"
}
def test_mithrix(self) -> None:
+3 -3
View File
@@ -957,13 +957,13 @@ def caclulate_soa_options(ctx: SC2Context) -> int:
return options
def kerrigan_primal(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]]) -> bool:
def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool:
if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg:
return True
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human:
return False
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35:
return items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] >= 35
return kerrigan_level >= 35
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion:
total_missions = len(ctx.mission_id_to_location_ids)
completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations
@@ -1138,7 +1138,7 @@ class ArchipelagoBot(bot.bot_ai.BotAI):
async def updateZergTech(self, current_items, kerrigan_level):
zerg_items = current_items[SC2Race.ZERG]
kerrigan_primal_by_items = kerrigan_primal(self.ctx, current_items)
kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level)
kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0
await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format(
kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2],

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