mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-29 10:10:09 -07:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ae486ad04 | |||
| 25a5e37cc6 | |||
| 860df6e658 | |||
| 16525c91b8 | |||
| 30cdde8605 | |||
| 38c54ba393 | |||
| 5da3a40964 | |||
| a1ef25455b | |||
| feb62b4af2 | |||
| 6dbeb6c658 | |||
| 09abc5beaa | |||
| fb1cf26118 | |||
| 842a15fd3c | |||
| f67e8497e0 | |||
| 19f1b265b1 | |||
| 1c14d1107f | |||
| 7b3727e945 | |||
| d1274c12b9 | |||
| 7c44d749d4 | |||
| f1765899c4 | |||
| 87b9f4a6fa | |||
| 480c15eea0 | |||
| d4ec4d32f0 | |||
| 50fb70d832 | |||
| ca5c0d9eb8 | |||
| 98e2d89a1c | |||
| 5bda265f43 | |||
| 9ef1fa825d | |||
| f5ff005360 | |||
| fb3035a78b | |||
| 3d5c21cec5 | |||
| feaae1db12 | |||
| 56ec0e902d | |||
| c8fd42f938 | |||
| e5eb54fb27 | |||
| fbeba1e470 | |||
| 11073dfdac | |||
| 7e660dbd23 | |||
| 1fc2c5ed4b | |||
| 242126b4b2 | |||
| 30dda013de | |||
| ea4d0abb7f | |||
| 9bbc49204d | |||
| b97cee4372 | |||
| 5dcafac861 | |||
| 8d28c34f95 | |||
| 0f2bd0fb85 | |||
| cf59cfaad0 | |||
| c534cb79b5 | |||
| 8952fbdc03 | |||
| 9012afeb75 | |||
| 401a6d9a42 | |||
| 5d4ed00452 | |||
| 0ba6d90bb8 | |||
| b007a42487 | |||
| 32c92e03e7 | |||
| 14437d653f | |||
| 1021df8b1b |
@@ -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": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.5"
|
||||
__version__ = "0.4.6"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
from typing import List, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Blueprint, abort
|
||||
from flask import Blueprint, abort, url_for
|
||||
|
||||
import worlds.Files
|
||||
from .. import cache
|
||||
from ..models import Room, Seed
|
||||
|
||||
@@ -21,12 +22,30 @@ def room_info(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
|
||||
def supports_apdeltapatch(game: str):
|
||||
return game in worlds.Files.AutoPatchRegister.patch_types
|
||||
downloads = []
|
||||
for slot in sorted(room.seed.slots):
|
||||
if slot.data and not supports_apdeltapatch(slot.game):
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
elif slot.data:
|
||||
slot_download = {
|
||||
"slot": slot.player_id,
|
||||
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
|
||||
}
|
||||
downloads.append(slot_download)
|
||||
return {
|
||||
"tracker": room.tracker,
|
||||
"players": get_players(room.seed),
|
||||
"last_port": room.last_port,
|
||||
"last_activity": room.last_activity,
|
||||
"timeout": room.timeout
|
||||
"timeout": room.timeout,
|
||||
"downloads": downloads,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -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
@@ -45,7 +45,15 @@ def create():
|
||||
}
|
||||
|
||||
game_options = {}
|
||||
visible: typing.Set[str] = set()
|
||||
visible_weighted: typing.Set[str] = set()
|
||||
|
||||
for option_name, option in all_options.items():
|
||||
if option.visibility & Options.Visibility.simple_ui:
|
||||
visible.add(option_name)
|
||||
if option.visibility & Options.Visibility.complex_ui:
|
||||
visible_weighted.add(option_name)
|
||||
|
||||
if option_name in handled_in_js:
|
||||
pass
|
||||
|
||||
@@ -116,8 +124,6 @@ def create():
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
@@ -156,12 +162,23 @@ def create():
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
filtered_player_options = player_options
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(player_options, f, indent=2, separators=(',', ': '))
|
||||
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
filtered_player_options["gameOptions"] = {
|
||||
option_name: option_data for option_name, option_data in game_options.items()
|
||||
if option_name in visible_weighted
|
||||
}
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in game_options.values():
|
||||
for option in filtered_player_options["gameOptions"].values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
|
||||
@@ -170,7 +187,7 @@ def create():
|
||||
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": game_options,
|
||||
"gameSettings": filtered_player_options["gameOptions"],
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<br /><br />
|
||||
|
||||
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||
You'll need to upload either a config file or a zip file containing one more config files.
|
||||
You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
|
||||
<br /><br />
|
||||
|
||||
If you have already generated a game and just need to host it, this site can<br />
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
@@ -221,7 +221,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> Li
|
||||
return items
|
||||
item = items.pop(0)
|
||||
multiworld.push_item(location, item, False)
|
||||
location.event = item.advancement
|
||||
|
||||
return items
|
||||
|
||||
@@ -489,7 +488,6 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
location.event = True
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
@@ -527,13 +525,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertFalse(locations[0].advancement)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertTrue(locations[1].advancement)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertTrue(locations[2].advancement)
|
||||
self.assertEqual(locations[3].item, basic_items[0])
|
||||
self.assertFalse(locations[3].event)
|
||||
self.assertFalse(locations[3].advancement)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
@@ -746,7 +744,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
for item in multiworld.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
self.assertFalse(item.location.advancement, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
|
||||
|
||||
from typing import Any, Optional, Protocol
|
||||
from ..graphics import FillType_Drawable, FillType_Vec
|
||||
from ..graphics.texture import FillType_Drawable, FillType_Vec
|
||||
|
||||
|
||||
class FillType_BindCallback(Protocol):
|
||||
|
||||
+6
-2
@@ -121,7 +121,11 @@ class AutoLogicRegister(type):
|
||||
def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any:
|
||||
start = time.perf_counter()
|
||||
ret = method(*args)
|
||||
if multiworld:
|
||||
with multiworld.observer(method, start):
|
||||
ret = method(*args)
|
||||
else:
|
||||
ret = method(*args)
|
||||
taken = time.perf_counter() - start
|
||||
if taken > 1.0:
|
||||
if player and multiworld:
|
||||
@@ -169,7 +173,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
for world_type in sorted(world_types, key=lambda world: world.__name__):
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
if stage_callable:
|
||||
_timed_call(stage_callable, multiworld, *args)
|
||||
_timed_call(stage_callable, multiworld, *args, multiworld=multiworld)
|
||||
|
||||
|
||||
class WebWorld:
|
||||
|
||||
@@ -64,7 +64,7 @@ class SuffixIdentifier:
|
||||
def __init__(self, *args: str):
|
||||
self.suffixes = args
|
||||
|
||||
def __call__(self, path: str):
|
||||
def __call__(self, path: str) -> bool:
|
||||
if isinstance(path, str):
|
||||
for suffix in self.suffixes:
|
||||
if path.endswith(suffix):
|
||||
|
||||
@@ -234,8 +234,11 @@ async def _run_game(rom: str):
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
try:
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
|
||||
|
||||
def launch() -> None:
|
||||
|
||||
@@ -2657,6 +2657,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
@@ -2815,6 +2819,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
||||
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
|
||||
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
|
||||
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
|
||||
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
|
||||
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
|
||||
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
|
||||
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
|
||||
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
|
||||
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
|
||||
|
||||
@@ -408,14 +408,16 @@ def create_inverted_regions(world, player):
|
||||
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
|
||||
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
|
||||
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
|
||||
'Turtle Rock Big Key Door']),
|
||||
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
|
||||
'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
@@ -253,10 +253,8 @@ def generate_itempool(world):
|
||||
region.locations.append(loc)
|
||||
|
||||
multiworld.push_item(loc, item_factory('Triforce', world), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
multiworld.get_location('Ganon', player).event = True
|
||||
multiworld.get_location('Ganon', player).locked = True
|
||||
event_pairs = [
|
||||
('Agahnim 1', 'Beat Agahnim 1'),
|
||||
@@ -273,7 +271,7 @@ def generate_itempool(world):
|
||||
location = multiworld.get_location(location_name, player)
|
||||
event = item_factory(event_name, world)
|
||||
multiworld.push_item(location, event, False)
|
||||
location.event = location.locked = True
|
||||
location.locked = True
|
||||
|
||||
|
||||
# set up item pool
|
||||
|
||||
@@ -2,7 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
|
||||
FreeText
|
||||
FreeText, Removed
|
||||
|
||||
|
||||
class GlitchesRequired(Choice):
|
||||
@@ -716,9 +716,8 @@ class BeemizerTrapChance(BeemizerRange):
|
||||
display_name = "Beemizer Trap Chance"
|
||||
|
||||
|
||||
class AllowCollect(Toggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
Off by default, because it currently crashes on real hardware."""
|
||||
class AllowCollect(DefaultOnToggle):
|
||||
"""Allows for !collect / co-op to auto-open chests containing items for other players."""
|
||||
display_name = "Allow Collection of checks for other players"
|
||||
|
||||
|
||||
@@ -796,4 +795,9 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud,
|
||||
|
||||
# removed:
|
||||
"goals": Removed,
|
||||
"smallkey_shuffle": Removed,
|
||||
"bigkey_shuffle": Removed,
|
||||
}
|
||||
|
||||
@@ -336,13 +336,15 @@ def create_regions(world, player):
|
||||
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
|
||||
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
|
||||
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
|
||||
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
|
||||
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
|
||||
|
||||
+9
-6
@@ -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
@@ -279,6 +279,9 @@ def global_rules(world, player):
|
||||
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
|
||||
)
|
||||
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
|
||||
@@ -477,7 +480,6 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(world.get_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, player) and can_kill_most_things(state, player, 10))
|
||||
set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
|
||||
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
@@ -487,6 +489,13 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
||||
|
||||
if not world.worlds[player].fix_trock_doors:
|
||||
add_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
set_rule(world.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||
|
||||
if world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
||||
@@ -1184,7 +1193,6 @@ def set_trock_key_rules(world, player):
|
||||
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
|
||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||
location.place_locked_item(item)
|
||||
location.event = True
|
||||
toss_junk_item(world, player)
|
||||
|
||||
if world.accessibility[player] != 'locations':
|
||||
|
||||
@@ -261,6 +261,10 @@ class ALTTPWorld(World):
|
||||
self.dungeons = {}
|
||||
self.waterfall_fairy_bottle_fill = "Bottle"
|
||||
self.pyramid_fairy_bottle_fill = "Bottle"
|
||||
self.fix_trock_doors = None
|
||||
self.fix_skullwoods_exit = None
|
||||
self.fix_palaceofdarkness_exit = None
|
||||
self.fix_trock_exit = None
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -280,6 +284,15 @@ class ALTTPWorld(World):
|
||||
player = self.player
|
||||
multiworld = self.multiworld
|
||||
|
||||
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
|
||||
or multiworld.mode[player] == 'inverted')
|
||||
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
||||
'dungeons_simple']
|
||||
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
|
||||
'simple', 'restricted']
|
||||
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
||||
'dungeons_simple']
|
||||
|
||||
# fairy bottle fills
|
||||
bottle_options = [
|
||||
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
||||
|
||||
@@ -101,20 +101,20 @@ class TestDeathMountain(TestInvertedOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Bottom Left", False, []],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Left", False, []],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
|
||||
["Hookshot Cave - Top Right", False, []],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
])
|
||||
@@ -177,7 +177,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Right", False, []],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -185,7 +185,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
|
||||
@@ -201,7 +201,7 @@ class TestDeathMountain(TestVanillaOWG):
|
||||
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Moon Pearl']],
|
||||
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
|
||||
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
|
||||
])
|
||||
@@ -1,4 +1,7 @@
|
||||
item_table = (
|
||||
'An Old GeoCities Profile',
|
||||
'Very Funny Joke',
|
||||
'Motivational Video',
|
||||
'Staples Easy Button',
|
||||
'One Million Dollars',
|
||||
'Replica Master Sword',
|
||||
@@ -13,7 +16,7 @@ item_table = (
|
||||
'2012 Magic the Gathering Core Set Starter Box',
|
||||
'Poke\'mon Booster Pack',
|
||||
'USB Speakers',
|
||||
'Plastic Spork',
|
||||
'Eco-Friendly Spork',
|
||||
'Cheeseburger',
|
||||
'Brand New Car',
|
||||
'Hunting Knife',
|
||||
@@ -22,7 +25,7 @@ item_table = (
|
||||
'One-Up Mushroom',
|
||||
'Nokia N-GAGE',
|
||||
'2-Liter of Sprite',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
|
||||
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!',
|
||||
'Can of Compressed Air',
|
||||
'Striped Kitten',
|
||||
'USB Power Adapter',
|
||||
|
||||
+10
-20
@@ -1,6 +1,5 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
from ..generic.Rules import set_rule
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class ArchipIDLELogic(LogicMixin):
|
||||
@@ -10,29 +9,20 @@ class ArchipIDLELogic(LogicMixin):
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
for i in range(16, 31):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 4)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 4)
|
||||
|
||||
for i in range(31, 51):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 10)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 10)
|
||||
|
||||
for i in range(51, 101):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 20)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 20)
|
||||
|
||||
for i in range(101, 201):
|
||||
set_rule(
|
||||
world.get_location(f"IDLE item number {i}", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 40)
|
||||
)
|
||||
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
|
||||
state: state._archipidle_location_is_accessible(player, 40)
|
||||
|
||||
world.completion_condition[player] =\
|
||||
lambda state:\
|
||||
state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
from .Items import item_table
|
||||
from .Rules import set_rules
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ArchipIDLEWebWorld(WebWorld):
|
||||
@@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld):
|
||||
|
||||
class ArchipIDLEWorld(World):
|
||||
"""
|
||||
An idle game which sends a check every thirty seconds, up to two hundred checks.
|
||||
An idle game which sends a check every thirty to sixty seconds, up to two hundred checks.
|
||||
"""
|
||||
game = "ArchipIDLE"
|
||||
topology_present = False
|
||||
data_version = 5
|
||||
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
|
||||
web = ArchipIDLEWebWorld()
|
||||
|
||||
@@ -56,18 +55,40 @@ class ArchipIDLEWorld(World):
|
||||
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self):
|
||||
item_table_copy = list(item_table)
|
||||
self.multiworld.random.shuffle(item_table_copy)
|
||||
|
||||
item_pool = []
|
||||
for i in range(200):
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 40 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
item_pool = [
|
||||
ArchipIDLEItem(
|
||||
item_table[0],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[0]],
|
||||
self.player
|
||||
)
|
||||
item_pool.append(item)
|
||||
]
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[1],
|
||||
ItemClassification.progression,
|
||||
self.item_name_to_id[item_table[1]],
|
||||
self.player
|
||||
))
|
||||
|
||||
for i in range(40):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table[2],
|
||||
ItemClassification.filler,
|
||||
self.item_name_to_id[item_table[2]],
|
||||
self.player
|
||||
))
|
||||
|
||||
item_table_copy = list(item_table[3:])
|
||||
self.random.shuffle(item_table_copy)
|
||||
for i in range(119):
|
||||
item_pool.append(ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 9 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -294,7 +294,7 @@ barnacle_region = "Barnacle's Island Region"
|
||||
blue_region = "Blue's Beach Hut Region"
|
||||
blizzard_region = "Bizzard's Basecamp Region"
|
||||
|
||||
lake_orangatanga_region = "Lake_Orangatanga"
|
||||
lake_orangatanga_region = "Lake Orangatanga"
|
||||
kremwood_forest_region = "Kremwood Forest"
|
||||
cotton_top_cove_region = "Cotton-Top Cove"
|
||||
mekanos_region = "Mekanos"
|
||||
|
||||
@@ -201,7 +201,12 @@ class DKC3World(World):
|
||||
er_hint_data = {}
|
||||
for world_index in range(len(world_names)):
|
||||
for level_index in range(5):
|
||||
level_region = self.multiworld.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
|
||||
level_id: int = world_index * 5 + level_index
|
||||
|
||||
if level_id >= len(self.active_level_list):
|
||||
break
|
||||
|
||||
level_region = self.multiworld.get_region(self.active_level_list[level_id], self.player)
|
||||
for location in level_region.locations:
|
||||
er_hint_data[location.address] = world_names[world_index]
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class DLCqworld(World):
|
||||
self.precollect_coinsanity()
|
||||
locations_count = len([location
|
||||
for location in self.multiworld.get_locations(self.player)
|
||||
if not location.event])
|
||||
if not location.advancement])
|
||||
|
||||
items_to_exclude = [excluded_items
|
||||
for excluded_items in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
@@ -10,7 +10,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]:
|
||||
|
||||
|
||||
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [location.name for location in multiworld.get_locations() if not location.event]
|
||||
return [location.name for location in multiworld.get_locations() if not location.advancement]
|
||||
|
||||
|
||||
def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
@@ -38,5 +38,5 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
|
||||
|
||||
def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
|
||||
non_event_locations = [location for location in multiworld.get_locations() if not location.advancement]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
@@ -90,7 +90,7 @@ def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typi
|
||||
if loc_name not in multiworld.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
if not location.event:
|
||||
if not location.advancement:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from functools import partial
|
||||
|
||||
|
||||
godhome_event_names = ["Godhome_Flower_Quest", "Defeated_Pantheon_5", "GG_Atrium_Roof", "Defeated_Pantheon_1", "Defeated_Pantheon_2", "Defeated_Pantheon_3", "Opened_Pantheon_4", "Defeated_Pantheon_4", "GG_Atrium", "Hit_Pantheon_5_Unlock_Orb", "GG_Workshop", "Can_Damage_Crystal_Guardian", 'Defeated_Any_Soul_Warrior', "Defeated_Colosseum_3", "COMBAT[Radiance]", "COMBAT[Pantheon_1]", "COMBAT[Pantheon_2]", "COMBAT[Pantheon_3]", "COMBAT[Pantheon_4]", "COMBAT[Pantheon_5]", "COMBAT[Colosseum_3]", 'Warp-Junk_Pit_to_Godhome', 'Bench-Godhome_Atrium', 'Bench-Hall_of_Gods', "GODTUNERUNLOCK", "GG_Waterways", "Warp-Godhome_to_Junk_Pit", "NAILCOMBAT", "BOSS", "AERIALMINIBOSS"]
|
||||
|
||||
|
||||
def set_godhome_rules(hk_world, hk_set_rule):
|
||||
player = hk_world.player
|
||||
fn = partial(hk_set_rule, hk_world)
|
||||
|
||||
required_events = {
|
||||
"Godhome_Flower_Quest": lambda state: state.count('Defeated_Pantheon_5', player) and state.count('Room_Mansion[left1]', player) and state.count('Fungus3_49[right1]', player),
|
||||
|
||||
"Defeated_Pantheon_5": lambda state: state.has('GG_Atrium_Roof', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and ((state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player) and state.has('COMBAT[Radiance]', player))),
|
||||
"GG_Atrium_Roof": lambda state: state.has('GG_Atrium', player) and state.has('Hit_Pantheon_5_Unlock_Orb', player) and state.has('LEFTCLAW', player),
|
||||
|
||||
"Defeated_Pantheon_1": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Gruz_Mother', player) and state.has('Defeated_False_Knight', player) and (state.has('Fungus1_29[left1]', player) or state.has('Fungus1_29[right1]', player)) and state.has('Defeated_Hornet_1', player) and state.has('Defeated_Gorb', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_Any_Soul_Warrior', player) and state.has('Defeated_Brooding_Mawlek', player))),
|
||||
"Defeated_Pantheon_2": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Xero', player) and state.has('Defeated_Crystal_Guardian', player) and state.has('Defeated_Soul_Master', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Mantis_Lords', player) and state.has('Defeated_Marmu', player) and state.has('Defeated_Nosk', player) and state.has('Defeated_Flukemarm', player) and state.has('Defeated_Broken_Vessel', player))),
|
||||
"Defeated_Pantheon_3": lambda state: state.has('GG_Atrium', player) and ((state.has('Defeated_Hive_Knight', player) and state.has('Defeated_Elder_Hu', player) and state.has('Defeated_Collector', player) and state.has('Defeated_Colosseum_2', player) and state.has('Defeated_Grimm', player) and state.has('Defeated_Galien', player) and state.has('Defeated_Uumuu', player) and state.has('Defeated_Hornet_2', player))),
|
||||
"Opened_Pantheon_4": lambda state: state.has('GG_Atrium', player) and (state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player)),
|
||||
"Defeated_Pantheon_4": lambda state: state.has('GG_Atrium', player) and state.has('Opened_Pantheon_4', player) and ((state.has('Defeated_Enraged_Guardian', player) and state.has('Defeated_Broken_Vessel', player) and state.has('Defeated_No_Eyes', player) and state.has('Defeated_Traitor_Lord', player) and state.has('Defeated_Dung_Defender', player) and state.has('Defeated_False_Knight', player) and state.has('Defeated_Markoth', player) and state.has('Defeated_Watcher_Knights', player) and state.has('Defeated_Soul_Master', player))),
|
||||
"GG_Atrium": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) and (state.has('RIGHTCLAW', player) or state.has('WINGS', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player)) or state.has('GG_Workshop', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player) and state.has('WINGS', player)) or state.has('Bench-Godhome_Atrium', player),
|
||||
"Hit_Pantheon_5_Unlock_Orb": lambda state: state.has('GG_Atrium', player) and state.has('WINGS', player) and (state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) and (((state.has('Queen_Fragment', player) and state.has('King_Fragment', player) and state.has('Void_Heart', player)) and state.has('Defeated_Pantheon_1', player) and state.has('Defeated_Pantheon_2', player) and state.has('Defeated_Pantheon_3', player) and state.has('Defeated_Pantheon_4', player))),
|
||||
"GG_Workshop": lambda state: state.has('GG_Atrium', player) or state.has('Bench-Hall_of_Gods', player),
|
||||
"Can_Damage_Crystal_Guardian": lambda state: state.has('UPSLASH', player) or state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) and (state.has('DREAMNAIL', player) and (state.has('SPELLS', player) or state.has('FOCUS', player) and state.has('Spore_Shroom', player) or state.has('Glowing_Womb', player)) or state.has('Weaversong', player)),
|
||||
'Defeated_Any_Soul_Warrior': lambda state: state.has('Defeated_Sanctum_Warrior', player) or state.has('Defeated_Elegant_Warrior', player) or state.has('Room_Colosseum_01[left1]', player) and state.has('Defeated_Colosseum_3', player),
|
||||
"Defeated_Colosseum_3": lambda state: state.has('Room_Colosseum_01[left1]', player) and state.has('Can_Replenish_Geo', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player)) or ((state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and state.has('WINGS', player))) and state.has('COMBAT[Colosseum_3]', player),
|
||||
|
||||
# MACROS
|
||||
"COMBAT[Radiance]": lambda state: (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_1]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_2]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player),
|
||||
"COMBAT[Pantheon_3]": lambda state: state.has('AERIALMINIBOSS', player) and state.count('SPELLS', player) > 1 and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_4]": lambda state: state.has('AERIALMINIBOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Pantheon_5]": lambda state: state.has('AERIALMINIBOSS', player) and state.has('FOCUS', player) and state.has('Can_Damage_Crystal_Guardian', player) and (state.has('LEFTDASH', player) and state.has('RIGHTDASH', player)) and ((((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('LEFTDASH', player)) and ((state.count('LEFTDASH', player) > 1 or state.count('RIGHTDASH', player) > 1) and state.has('RIGHTDASH', player))) or state.has('QUAKE', player)) and (state.count('FIREBALL', player) > 1 and state.has('UPSLASH', player) or state.count('SCREAM', player) > 1 and state.has('UPSLASH', player) or state._hk_option(player, 'RemoveSpellUpgrades') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('UPSLASH', player) or (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
"COMBAT[Colosseum_3]": lambda state: state.has('BOSS', player) and (state.has('FOCUS', player) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat'))),
|
||||
|
||||
# MISC
|
||||
'Warp-Junk_Pit_to_Godhome': lambda state: state.has('GG_Waterways', player) and state.has('GODTUNERUNLOCK', player) and state.has('DREAMNAIL', player),
|
||||
'Bench-Godhome_Atrium': lambda state: state.has('GG_Atrium', player) and (state.has('RIGHTCLAW', player) and (state.has('RIGHTDASH', player) or state.has('LEFTCLAW', player) and state.has('RIGHTSUPERDASH', player) or state.has('WINGS', player)) or state.has('LEFTCLAW', player) and state.has('WINGS', player)),
|
||||
'Bench-Hall_of_Gods': lambda state: state.has('GG_Workshop', player) and ((state.has('LEFTCLAW', player) or state.has('RIGHTCLAW', player))),
|
||||
|
||||
"GODTUNERUNLOCK": lambda state: state.count('SIMPLE', player) > 3,
|
||||
"GG_Waterways": lambda state: state.has('GG_Waterways[door1]', player) or state.has('GG_Waterways[right1]', player) and (state.has('LEFTSUPERDASH', player) or state.has('SWIM', player)) or state.has('Warp-Godhome_to_Junk_Pit', player),
|
||||
"Warp-Godhome_to_Junk_Pit": lambda state: state.has('Warp-Junk_Pit_to_Godhome', player) or state.has('GG_Atrium', player),
|
||||
|
||||
# COMBAT MACROS
|
||||
"NAILCOMBAT": lambda state: (state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state._hk_option(player, 'ProficientCombat') and (state.has('CYCLONE', player) or state.has('Great_Slash', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')),
|
||||
"BOSS": lambda state: state.count('SPELLS', player) > 1 and ((state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and state.has('NAILCOMBAT', player)),
|
||||
"AERIALMINIBOSS": lambda state: (state.has('FIREBALL', player) or state.has('SCREAM', player)) and (state.has('LEFTDASH', player) or state.has('RIGHTDASH', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or state._hk_option(player, 'ProficientCombat') and (state.has('FIREBALL', player) or state.has('SCREAM', player)) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player)) or (state._hk_option(player, 'DifficultSkips') and state._hk_option(player, 'ProficientCombat')) and ((state.has('LEFTSLASH', player) or state.has('RIGHTSLASH', player)) or state.has('UPSLASH', player) or state.has('CYCLONE', player) or state.has('Great_Slash', player)),
|
||||
|
||||
}
|
||||
|
||||
for item, rule in required_events.items():
|
||||
fn(item, rule)
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Dict, Set, NamedTuple
|
||||
from .ExtractedData import items, logic_items, item_effects
|
||||
from .GodhomeData import godhome_event_names
|
||||
|
||||
item_table = {}
|
||||
|
||||
@@ -14,6 +15,9 @@ for i, (item_name, item_type) in enumerate(items.items(), start=0x1000000):
|
||||
item_table[item_name] = HKItemData(advancement=item_name in logic_items or item_name in item_effects,
|
||||
id=i, type=item_type)
|
||||
|
||||
for item_name in godhome_event_names:
|
||||
item_table[item_name] = HKItemData(advancement=True, id=None, type=None)
|
||||
|
||||
lookup_id_to_name: Dict[int, str] = {data.id: item_name for item_name, data in item_table.items()}
|
||||
lookup_type_to_names: Dict[str, Set[str]] = {}
|
||||
for item, item_data in item_table.items():
|
||||
|
||||
@@ -397,8 +397,8 @@ class Goal(Choice):
|
||||
option_hollowknight = 1
|
||||
option_siblings = 2
|
||||
option_radiance = 3
|
||||
# Client support exists for this, but logic is a nightmare
|
||||
# option_godhome = 4
|
||||
option_godhome = 4
|
||||
option_godhome_flower = 5
|
||||
default = 0
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from ..generic.Rules import set_rule, add_rule
|
||||
from ..AutoWorld import World
|
||||
from .GeneratedRules import set_generated_rules
|
||||
from .GodhomeData import set_godhome_rules
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
@@ -39,6 +40,7 @@ def hk_set_rule(hk_world: World, location: str, rule):
|
||||
def set_rules(hk_world: World):
|
||||
player = hk_world.player
|
||||
set_generated_rules(hk_world, hk_set_rule)
|
||||
set_godhome_rules(hk_world, hk_set_rule)
|
||||
|
||||
# Shop costs
|
||||
for location in hk_world.multiworld.get_locations(player):
|
||||
|
||||
@@ -307,6 +307,12 @@ class HKWorld(World):
|
||||
randomized = True
|
||||
_add("Elevator_Pass", "Elevator_Pass", randomized)
|
||||
|
||||
# check for any goal that godhome events are relevant to
|
||||
if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]:
|
||||
from .GodhomeData import godhome_event_names
|
||||
for item_name in godhome_event_names:
|
||||
_add(item_name, item_name, False)
|
||||
|
||||
for shop, locations in self.created_multi_locations.items():
|
||||
for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value):
|
||||
loc = self.create_location(shop)
|
||||
@@ -431,6 +437,10 @@ class HKWorld(World):
|
||||
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
|
||||
elif goal == Goal.option_radiance:
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
|
||||
elif goal == Goal.option_godhome:
|
||||
world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
|
||||
elif goal == Goal.option_godhome_flower:
|
||||
world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
|
||||
else:
|
||||
# Any goal
|
||||
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
|
||||
|
||||
@@ -413,26 +413,31 @@ def set_rules(hylics2world):
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
|
||||
lambda state: paddle(state, player))
|
||||
|
||||
@@ -110,7 +110,11 @@ def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]):
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
|
||||
world.player).parent_region.add_exits([first_rooms[proper_stage].name])
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
if world.options.open_world:
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
|
||||
.parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
|
||||
|
||||
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
|
||||
|
||||
@@ -757,7 +757,7 @@ class Assembler:
|
||||
|
||||
def const(name: str, value: int) -> None:
|
||||
name = name.upper()
|
||||
assert name not in CONST_MAP
|
||||
assert name not in CONST_MAP or CONST_MAP[name] == value
|
||||
CONST_MAP[name] = value
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ from .locations.keyLocation import KeyLocation
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import LinksAwakeningLocation
|
||||
from ..Options import TrendyGame, Palette, MusicChangeCondition
|
||||
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
|
||||
|
||||
|
||||
# Function to generate a final rom, this patches the rom with all required patches
|
||||
@@ -97,7 +97,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
||||
assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have
|
||||
assembler.const("wSeashellsCount", 0xDB41)
|
||||
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available
|
||||
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
|
||||
assembler.const("wCustomMessage", 0xC0A0)
|
||||
|
||||
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
|
||||
@@ -243,6 +243,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
|
||||
patches.core.quickswap(rom, 1)
|
||||
elif settings.quickswap == 'b':
|
||||
patches.core.quickswap(rom, 0)
|
||||
|
||||
patches.core.addBootsControls(rom, ap_settings['boots_controls'])
|
||||
|
||||
|
||||
world_setup = logic.world_setup
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ class StartItem(DroppedKey):
|
||||
# We need to give something here that we can use to progress.
|
||||
# FEATHER
|
||||
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
|
||||
|
||||
MULTIWORLD = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -51,7 +51,7 @@ GiveItemFromChest:
|
||||
dw ChestBow ; CHEST_BOW
|
||||
dw ChestWithItem ; CHEST_HOOKSHOT
|
||||
dw ChestWithItem ; CHEST_MAGIC_ROD
|
||||
dw ChestWithItem ; CHEST_PEGASUS_BOOTS
|
||||
dw Boots ; CHEST_PEGASUS_BOOTS
|
||||
dw ChestWithItem ; CHEST_OCARINA
|
||||
dw ChestWithItem ; CHEST_FEATHER
|
||||
dw ChestWithItem ; CHEST_SHOVEL
|
||||
@@ -273,6 +273,13 @@ ChestMagicPowder:
|
||||
ld [$DB4C], a
|
||||
jp ChestWithItem
|
||||
|
||||
Boots:
|
||||
; We use DB6D to store which tunics we have available
|
||||
; ...and the boots
|
||||
ld a, [wCollectedTunics]
|
||||
or $04
|
||||
ld [wCollectedTunics], a
|
||||
jp ChestWithItem
|
||||
|
||||
Flippers:
|
||||
ld a, $01
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from .. import assembler
|
||||
from ..assembler import ASM
|
||||
from ..entranceInfo import ENTRANCE_INFO
|
||||
from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal
|
||||
from ..backgroundEditor import BackgroundEditor
|
||||
from .. import utils
|
||||
|
||||
from ...Options import BootsControls
|
||||
|
||||
def bugfixWrittingWrongRoomStatus(rom):
|
||||
# The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in
|
||||
@@ -391,7 +393,7 @@ OAMData:
|
||||
db $20, $20, $20, $00 ;I
|
||||
db $20, $28, $28, $00 ;M
|
||||
db $20, $30, $18, $00 ;E
|
||||
|
||||
|
||||
db $20, $70, $16, $00 ;D
|
||||
db $20, $78, $18, $00 ;E
|
||||
db $20, $80, $10, $00 ;A
|
||||
@@ -408,7 +410,7 @@ OAMData:
|
||||
db $68, $38, $%02x, $00 ;0
|
||||
db $68, $40, $%02x, $00 ;0
|
||||
db $68, $48, $%02x, $00 ;0
|
||||
|
||||
|
||||
""" % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True)
|
||||
# Lower line of credits roll into XX XX XX
|
||||
rom.patch(0x17, 0x0784, 0x082D, ASM("""
|
||||
@@ -425,7 +427,7 @@ OAMData:
|
||||
call updateOAM
|
||||
ld a, [$B001] ; seconds
|
||||
call updateOAM
|
||||
|
||||
|
||||
ld a, [$DB58] ; death count high
|
||||
call updateOAM
|
||||
ld a, [$DB57] ; death count low
|
||||
@@ -473,7 +475,7 @@ OAMData:
|
||||
db $68, $18, $40, $00 ;0
|
||||
db $68, $20, $40, $00 ;0
|
||||
db $68, $28, $40, $00 ;0
|
||||
|
||||
|
||||
""", 0x4784), fill_nop=True)
|
||||
|
||||
# Grab the "mostly" complete A-Z font
|
||||
@@ -539,6 +541,97 @@ OAMData:
|
||||
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
|
||||
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
|
||||
|
||||
def addBootsControls(rom, boots_controls: BootsControls):
|
||||
if boots_controls == BootsControls.option_vanilla:
|
||||
return
|
||||
consts = {
|
||||
"INVENTORY_PEGASUS_BOOTS": 0x8,
|
||||
"INVENTORY_POWER_BRACELET": 0x3,
|
||||
"UsePegasusBoots": 0x1705,
|
||||
"J_A": (1 << 4),
|
||||
"J_B": (1 << 5),
|
||||
"wAButtonSlot": 0xDB01,
|
||||
"wBButtonSlot": 0xDB00,
|
||||
"wPegasusBootsChargeMeter": 0xC14B,
|
||||
"hPressedButtonsMask": 0xCB
|
||||
}
|
||||
for c,v in consts.items():
|
||||
assembler.const(c, v)
|
||||
|
||||
BOOTS_START_ADDR = 0x11E8
|
||||
condition = {
|
||||
BootsControls.option_bracelet: """
|
||||
ld a, [hl]
|
||||
; Check if we are using the bracelet
|
||||
cp INVENTORY_POWER_BRACELET
|
||||
jr z, .yesBoots
|
||||
""",
|
||||
BootsControls.option_press_a: """
|
||||
; Check if we are using the A slot
|
||||
cp J_A
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
""",
|
||||
BootsControls.option_press_b: """
|
||||
; Check if we are using the B slot
|
||||
cp J_B
|
||||
jr z, .yesBoots
|
||||
ld a, [hl]
|
||||
"""
|
||||
}[boots_controls.value]
|
||||
|
||||
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
|
||||
boots_code = assembler.ASM("""
|
||||
CheckBoots:
|
||||
; check if we own boots
|
||||
ld a, [wCollectedTunics]
|
||||
and $04
|
||||
; if not, move on to the next inventory item (shield)
|
||||
jr z, .out
|
||||
|
||||
; Check the B button
|
||||
ld hl, wBButtonSlot
|
||||
ld d, J_B
|
||||
call .maybeBoots
|
||||
|
||||
; Check the A button
|
||||
inc l ; l = wAButtonSlot - done this way to save a byte or two
|
||||
ld d, J_A
|
||||
call .maybeBoots
|
||||
|
||||
; If neither, reset charge meter and bail
|
||||
xor a
|
||||
ld [wPegasusBootsChargeMeter], a
|
||||
jr .out
|
||||
|
||||
.maybeBoots:
|
||||
; Check if we are holding this button even
|
||||
ldh a, [hPressedButtonsMask]
|
||||
and d
|
||||
ret z
|
||||
"""
|
||||
# Check the special condition (also loads the current item for button into a)
|
||||
+ condition +
|
||||
"""
|
||||
; Check if we are just using boots regularly
|
||||
cp INVENTORY_PEGASUS_BOOTS
|
||||
ret nz
|
||||
.yesBoots:
|
||||
; We're using boots! Do so.
|
||||
call UsePegasusBoots
|
||||
; If we return now we will go back into CheckBoots, we don't want that
|
||||
; We instead want to move onto the next item
|
||||
; but if we don't cleanup, the next "ret" will take us back there again
|
||||
; So we pop the return address off of the stack
|
||||
pop af
|
||||
.out:
|
||||
""", BOOTS_START_ADDR)
|
||||
|
||||
|
||||
|
||||
original_code = 'fa00dbfe08200ff0cbe6202805cd05171804afea4bc1fa01dbfe08200ff0cbe6102805cd05171804afea4bc1'
|
||||
rom.patch(0, BOOTS_START_ADDR, original_code, boots_code, fill_nop=True)
|
||||
|
||||
def addWarpImprovements(rom, extra_warps):
|
||||
# Patch in a warp icon
|
||||
tile = utils.createTileData( \
|
||||
@@ -739,4 +832,3 @@ success:
|
||||
exit:
|
||||
ret
|
||||
"""))
|
||||
|
||||
|
||||
@@ -60,13 +60,11 @@ class LinksAwakeningLocation(Location):
|
||||
|
||||
def __init__(self, player: int, region, ladxr_item):
|
||||
name = meta_to_name(ladxr_item.metadata)
|
||||
|
||||
self.event = ladxr_item.event is not None
|
||||
if self.event:
|
||||
name = ladxr_item.event
|
||||
|
||||
address = None
|
||||
if not self.event:
|
||||
|
||||
if ladxr_item.event is not None:
|
||||
name = ladxr_item.event
|
||||
else:
|
||||
address = locations_to_id[name]
|
||||
super().__init__(player, name, address)
|
||||
self.parent_region = region
|
||||
|
||||
+16
-1
@@ -316,6 +316,21 @@ class Overworld(Choice, LADXROption):
|
||||
# [Disable] no music in the whole game""",
|
||||
# aesthetic=True),
|
||||
|
||||
class BootsControls(Choice):
|
||||
"""
|
||||
Adds additional button to activate Pegasus Boots (does nothing if you haven't picked up your boots!)
|
||||
[Vanilla] Nothing changes, you have to equip the boots to use them
|
||||
[Bracelet] Holding down the button for the bracelet also activates boots (somewhat like Link to the Past)
|
||||
[Press A] Holding down A activates boots
|
||||
[Press B] Holding down B activates boots
|
||||
"""
|
||||
display_name = "Boots Controls"
|
||||
option_vanilla = 0
|
||||
option_bracelet = 1
|
||||
option_press_a = 2
|
||||
option_press_b = 3
|
||||
|
||||
|
||||
class LinkPalette(Choice, LADXROption):
|
||||
"""
|
||||
Sets link's palette
|
||||
@@ -485,5 +500,5 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
||||
'music_change_condition': MusicChangeCondition,
|
||||
'nag_messages': NagMessages,
|
||||
'ap_title_screen': APTitleScreen,
|
||||
|
||||
'boots_controls': BootsControls,
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class LinksAwakeningWorld(World):
|
||||
# Place RAFT, other access events
|
||||
for region in regions:
|
||||
for loc in region.locations:
|
||||
if loc.event:
|
||||
if loc.address is None:
|
||||
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
|
||||
|
||||
# Connect Windfish -> Victory
|
||||
|
||||
@@ -63,7 +63,7 @@ class LingoWorld(World):
|
||||
self.player_logic = LingoPlayerLogic(self)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self, self.player_logic)
|
||||
create_regions(self)
|
||||
|
||||
def create_items(self):
|
||||
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
||||
|
||||
+17
-21
@@ -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
@@ -2,61 +2,58 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .datatypes import RoomAndDoor
|
||||
from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation
|
||||
from .player_logic import AccessRequirements, PlayerLocation
|
||||
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld"):
|
||||
if door is None:
|
||||
return True
|
||||
|
||||
effective_room = room if door.room is None else door.room
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
|
||||
return _lingo_can_open_door(state, effective_room, door.door, world)
|
||||
|
||||
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"):
|
||||
return _lingo_can_satisfy_requirements(state, location.access, world)
|
||||
|
||||
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
|
||||
satisfied_count = 0
|
||||
for access_req in player_logic.mastery_reqs:
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
for access_req in world.player_logic.mastery_reqs:
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world):
|
||||
satisfied_count += 1
|
||||
return satisfied_count >= world.options.mastery_achievements.value
|
||||
|
||||
|
||||
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"):
|
||||
counted_panels = 0
|
||||
state.update_reachable_regions(world.player)
|
||||
for region in state.reachable_regions[world.player]:
|
||||
for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []):
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
|
||||
for access_req, panel_count in world.player_logic.counting_panel_reqs.get(region.name, []):
|
||||
if _lingo_can_satisfy_requirements(state, access_req, world):
|
||||
counted_panels += panel_count
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
# THE MASTER has to be handled separately, because it has special access rules.
|
||||
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
|
||||
and lingo_can_use_mastery_location(state, world, player_logic):
|
||||
and lingo_can_use_mastery_location(state, world):
|
||||
counted_panels += 1
|
||||
if counted_panels >= world.options.level_2_requirement.value - 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld"):
|
||||
for req_room in access.rooms:
|
||||
if not state.can_reach(req_room, "Region", world.player):
|
||||
return False
|
||||
|
||||
for req_door in access.doors:
|
||||
if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic):
|
||||
if not _lingo_can_open_door(state, req_door.room, req_door.door, world):
|
||||
return False
|
||||
|
||||
if len(access.colors) > 0 and world.options.shuffle_colors:
|
||||
@@ -67,15 +64,14 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
|
||||
return True
|
||||
|
||||
|
||||
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld"):
|
||||
"""
|
||||
Determines whether a door can be opened
|
||||
"""
|
||||
if door not in player_logic.item_by_door.get(room, {}):
|
||||
return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic)
|
||||
if door not in world.player_logic.item_by_door.get(room, {}):
|
||||
return _lingo_can_satisfy_requirements(state, world.player_logic.door_reqs[room][door], world)
|
||||
|
||||
item_name = player_logic.item_by_door[room][door]
|
||||
item_name = world.player_logic.item_by_door[room][door]
|
||||
if item_name in PROGRESSIVE_ITEMS:
|
||||
progression = PROGRESSION_BY_ROOM[room][door]
|
||||
return state.has(item_name, world.player, progression.index)
|
||||
@@ -83,12 +79,12 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L
|
||||
return state.has(item_name, world.player)
|
||||
|
||||
|
||||
def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
if location.name == player_logic.mastery_location:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world, player_logic)
|
||||
def make_location_lambda(location: PlayerLocation, world: "LingoWorld"):
|
||||
if location.name == world.player_logic.mastery_location:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world)
|
||||
|
||||
if world.options.level_2_requirement > 1\
|
||||
and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location):
|
||||
return lambda state: lingo_can_use_level_2_location(state, world, player_logic)
|
||||
and (location.name == "Second Room - ANOTHER TRY" or location.name == world.player_logic.level_2_location):
|
||||
return lambda state: lingo_can_use_level_2_location(state, world)
|
||||
|
||||
return lambda state: lingo_can_use_location(state, location, world, player_logic)
|
||||
return lambda state: lingo_can_use_location(state, location, world)
|
||||
|
||||
@@ -593,6 +593,20 @@ class HealingFloorChance(Range):
|
||||
default = 16
|
||||
|
||||
|
||||
class InactiveExpGain(Choice):
|
||||
"""The rate at which characters not currently in the active party gain EXP.
|
||||
|
||||
Supported values: disabled, half, full
|
||||
Default value: disabled (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Inactive character EXP gain"
|
||||
option_disabled = 0
|
||||
option_half = 50
|
||||
option_full = 100
|
||||
default = option_disabled
|
||||
|
||||
|
||||
class InitialFloor(Range):
|
||||
"""The initial floor, where you begin your journey.
|
||||
|
||||
@@ -805,7 +819,7 @@ class ShufflePartyMembers(Toggle):
|
||||
false — all 6 optional party members are present in the cafe and can be recruited right away
|
||||
true — only Maxim is available from the start; 6 new "items" are added to your pool and shuffled into the
|
||||
multiworld; when one of these items is found, the corresponding party member is unlocked for you to use.
|
||||
While cave diving, you can add newly unlocked ones to your party by using the character items from the inventory
|
||||
While cave diving, you can add or remove unlocked party members by using the character items from the inventory
|
||||
Default value: false (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
@@ -838,6 +852,7 @@ class L2ACOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
gold_modifier: GoldModifier
|
||||
healing_floor_chance: HealingFloorChance
|
||||
inactive_exp_gain: InactiveExpGain
|
||||
initial_floor: InitialFloor
|
||||
iris_floor_chance: IrisFloorChance
|
||||
iris_treasures_required: IrisTreasuresRequired
|
||||
|
||||
@@ -232,6 +232,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28001B:0x28001B + 1] = self.o.inactive_exp_gain.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
|
||||
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
|
||||
|
||||
@@ -309,6 +309,12 @@ org $8EFD2E ; unused region at the end of bank $8E
|
||||
DB $1E,$0B,$01,$2B,$05,$1A,$05,$00 ; add dekar
|
||||
DB $1E,$0B,$01,$2B,$04,$1A,$06,$00 ; add tia
|
||||
DB $1E,$0B,$01,$2B,$06,$1A,$07,$00 ; add lexis
|
||||
DB $1F,$0B,$01,$2C,$01,$1B,$02,$00 ; remove selan
|
||||
DB $1F,$0B,$01,$2C,$02,$1B,$03,$00 ; remove guy
|
||||
DB $1F,$0B,$01,$2C,$03,$1B,$04,$00 ; remove arty
|
||||
DB $1F,$0B,$01,$2C,$05,$1B,$05,$00 ; remove dekar
|
||||
DB $1F,$0B,$01,$2C,$04,$1B,$06,$00 ; remove tia
|
||||
DB $1F,$0B,$01,$2C,$06,$1B,$07,$00 ; remove lexis
|
||||
pullpc
|
||||
|
||||
SpecialItemUse:
|
||||
@@ -328,11 +334,15 @@ SpecialItemUse:
|
||||
SEP #$20
|
||||
LDA $8ED8C7,X ; load predefined bitmask with a single bit set
|
||||
BIT $077E ; check against EV flags $02 to $07 (party member flags)
|
||||
BNE + ; abort if character already present
|
||||
LDA $07A9 ; load EV register $11 (party counter)
|
||||
BEQ ++
|
||||
LDA.b #$30 ; character already present; modify pointer to point to L2SASM leave script
|
||||
ADC $09B7
|
||||
STA $09B7
|
||||
BRA +++
|
||||
++: LDA $07A9 ; character not present; load EV register $0B (party counter)
|
||||
CMP.b #$03
|
||||
BPL + ; abort if party full
|
||||
LDA.b #$8E
|
||||
+++ LDA.b #$8E
|
||||
STA $09B9
|
||||
PHK
|
||||
PEA ++
|
||||
@@ -340,7 +350,6 @@ SpecialItemUse:
|
||||
JML $83BB76 ; initialize parser variables
|
||||
++: NOP
|
||||
JSL $809CB8 ; call L2SASM parser
|
||||
JSL $81F034 ; consume the item
|
||||
TSX
|
||||
INX #13
|
||||
TXS
|
||||
@@ -490,6 +499,73 @@ pullpc
|
||||
|
||||
|
||||
|
||||
; allow inactive characters to gain exp
|
||||
pushpc
|
||||
org $81DADD
|
||||
; DB=$81, x=0, m=1
|
||||
NOP ; overwrites BNE $81DAE2 : JMP $DBED
|
||||
JML HandleActiveExp
|
||||
AwardExp:
|
||||
; isolate exp distribution into a subroutine, to be reused for both active party members and inactive characters
|
||||
org $81DAE9
|
||||
NOP #2 ; overwrites JMP $DBBD
|
||||
RTL
|
||||
org $81DB42
|
||||
NOP #2 ; overwrites JMP $DBBD
|
||||
RTL
|
||||
org $81DD11
|
||||
; DB=$81, x=0, m=1
|
||||
JSL HandleInactiveExp ; overwrites LDA $0A8A : CLC
|
||||
pullpc
|
||||
|
||||
HandleActiveExp:
|
||||
BNE + ; (overwritten instruction; modified) check if statblock not empty
|
||||
JML $81DBED ; (overwritten instruction; modified) abort
|
||||
+: JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
|
||||
JML $81DBBD ; (overwritten instruction; modified) continue to next level text
|
||||
|
||||
HandleInactiveExp:
|
||||
LDA $F0201B ; load inactive exp gain rate
|
||||
BEQ + ; zero gain; skip everything
|
||||
CMP.b #$64
|
||||
BCS ++ ; full gain
|
||||
LSR $1607
|
||||
ROR $1606 ; half gain
|
||||
ROR $1605
|
||||
++: LDY.w #$0000 ; start looping through all characters
|
||||
-: TDC
|
||||
TYA
|
||||
LDX.w #$0003 ; start looping through active party
|
||||
--: CMP $0A7B,X
|
||||
BEQ ++ ; skip if character in active party
|
||||
DEX
|
||||
BPL -- ; continue looping through active party
|
||||
STA $153D ; inactive character detected; overwrite character index of 1st slot in party battle order
|
||||
ASL
|
||||
TAX
|
||||
REP #$20
|
||||
LDA $859EBA,X ; convert character index to statblock pointer
|
||||
SEP #$20
|
||||
TAX
|
||||
PHY ; stash character loop index
|
||||
LDY $0A80
|
||||
PHY ; stash 1st (in menu order) party member statblock pointer
|
||||
STX $0A80 ; overwrite 1st (in menu order) party member statblock pointer
|
||||
LDY.w #$0000 ; set to use 1st position (in battle order)
|
||||
STY $00 ; set to use 1st position (in menu order)
|
||||
JSL AwardExp ; award exp (X=statblock pointer, Y=position in battle order, $00=position in menu order)
|
||||
PLY ; restore 1st (in menu order) party member statblock pointer
|
||||
STY $0A80
|
||||
PLY ; restore character loop index
|
||||
++: INY
|
||||
CPY.w #$0007
|
||||
BCC - ; continue looping through all characters
|
||||
+: LDA $0A8A ; (overwritten instruction) load current gold
|
||||
CLC ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
|
||||
|
||||
; receive death link
|
||||
pushpc
|
||||
org $83BC91
|
||||
@@ -1226,6 +1302,7 @@ pullpc
|
||||
; $F02018 1 party members available
|
||||
; $F02019 1 capsule monsters available
|
||||
; $F0201A 1 shop interval
|
||||
; $F0201B 1 inactive exp gain rate
|
||||
; $F02030 1 selected goal
|
||||
; $F02031 1 goal completion: boss
|
||||
; $F02032 1 goal completion: iris_treasure_hunt
|
||||
|
||||
Binary file not shown.
@@ -53,8 +53,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
||||
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
|
||||
- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
|
||||
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
|
||||
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
|
||||
by using the character items from your inventory
|
||||
find them in order to unlock them for you to use. While cave diving, you can add or remove unlocked party members by
|
||||
using the character items from your inventory. There's also an option to allow inactive characters to gain some EXP,
|
||||
so that new party members added during a run don't have to start off at a low level
|
||||
|
||||
###### Quality of life:
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ from BaseClasses import Location
|
||||
class MeritousLocation(Location):
|
||||
game: str = "Meritous"
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||
super(MeritousLocation, self).__init__(player, name, address, parent)
|
||||
if "Wervyn Anixil" in name or "Defeat" in name:
|
||||
self.event = True
|
||||
|
||||
|
||||
offset = 593_000
|
||||
|
||||
|
||||
@@ -35,13 +35,14 @@ class MuseDashCollections:
|
||||
"Rush-Hour",
|
||||
"Find this Month's Featured Playlist",
|
||||
"PeroPero in the Universe",
|
||||
"umpopoff"
|
||||
"umpopoff",
|
||||
"P E R O P E R O Brother Dance",
|
||||
]
|
||||
|
||||
REMOVED_SONGS = [
|
||||
"CHAOS Glitch",
|
||||
"FM 17314 SUGAR RADIO",
|
||||
"Yume Ou Mono Yo Secret"
|
||||
"Yume Ou Mono Yo Secret",
|
||||
]
|
||||
|
||||
album_items: Dict[str, AlbumData] = {}
|
||||
@@ -57,6 +58,7 @@ class MuseDashCollections:
|
||||
"Chromatic Aberration Trap": STARTING_CODE + 5,
|
||||
"Background Freeze Trap": STARTING_CODE + 6,
|
||||
"Gray Scale Trap": STARTING_CODE + 7,
|
||||
"Focus Line Trap": STARTING_CODE + 10,
|
||||
}
|
||||
|
||||
sfx_trap_items: Dict[str, int] = {
|
||||
@@ -64,7 +66,19 @@ class MuseDashCollections:
|
||||
"Error SFX Trap": STARTING_CODE + 9,
|
||||
}
|
||||
|
||||
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
|
||||
filler_items: Dict[str, int] = {
|
||||
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
|
||||
"Miss To Great (5 Pack)": STARTING_CODE + 31,
|
||||
"Extra Life": STARTING_CODE + 32,
|
||||
}
|
||||
|
||||
filler_item_weights: Dict[str, int] = {
|
||||
"Great To Perfect (10 Pack)": 10,
|
||||
"Miss To Great (5 Pack)": 3,
|
||||
"Extra Life": 1,
|
||||
}
|
||||
|
||||
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
|
||||
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
@@ -518,7 +518,7 @@ Haunted Dance|43-48|MD Plus Project|False|6|9|11|
|
||||
Hey Vincent.|43-49|MD Plus Project|True|6|8|10|
|
||||
Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9|
|
||||
Narcissism Angel|43-51|MD Plus Project|True|1|3|6|
|
||||
AlterLuna|43-52|MD Plus Project|True|6|8|11|
|
||||
AlterLuna|43-52|MD Plus Project|True|6|8|11|12
|
||||
Niki Tousen|43-53|MD Plus Project|True|6|8|10|11
|
||||
Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9|
|
||||
Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10|
|
||||
@@ -537,4 +537,11 @@ Ruler Of My Heart VIVINOS|71-1|Valentine Stage|False|2|4|6|
|
||||
Reality Show|71-2|Valentine Stage|False|5|7|10|
|
||||
SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
|
||||
Rose Love|71-4|Valentine Stage|True|2|4|7|
|
||||
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
||||
Euphoria|71-5|Valentine Stage|True|1|3|6|
|
||||
P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0|
|
||||
PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
|
||||
How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11
|
||||
Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12
|
||||
Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10|
|
||||
DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11|
|
||||
Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9|
|
||||
|
||||
@@ -4,11 +4,13 @@ from dataclasses import dataclass
|
||||
|
||||
from .MuseDashCollection import MuseDashCollections
|
||||
|
||||
|
||||
class AllowJustAsPlannedDLCSongs(Toggle):
|
||||
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
|
||||
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
|
||||
display_name = "Allow [Muse Plus] DLC Songs"
|
||||
|
||||
|
||||
class DLCMusicPacks(OptionSet):
|
||||
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
|
||||
display_name = "DLC Packs"
|
||||
@@ -101,20 +103,10 @@ class GradeNeeded(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class AdditionalItemPercentage(Range):
|
||||
"""The percentage of songs that will have 2 items instead of 1 when completing them.
|
||||
- Starting Songs will always have 2 items.
|
||||
- Locations will be filled with duplicate songs if there are not enough items.
|
||||
"""
|
||||
display_name = "Additional Item %"
|
||||
range_start = 50
|
||||
default = 80
|
||||
range_end = 100
|
||||
|
||||
|
||||
class MusicSheetCountPercentage(Range):
|
||||
"""Collecting enough Music Sheets will unlock the goal song needed for completion.
|
||||
This option controls how many are in the item pool, based on the total number of songs."""
|
||||
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
|
||||
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
|
||||
"""
|
||||
range_start = 10
|
||||
range_end = 40
|
||||
default = 20
|
||||
@@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
|
||||
streamer_mode_enabled: StreamerModeEnabled
|
||||
starting_song_count: StartingSongs
|
||||
additional_song_count: AdditionalSongs
|
||||
additional_item_percentage: AdditionalItemPercentage
|
||||
song_difficulty_mode: DifficultyMode
|
||||
song_difficulty_min: DifficultyModeOverrideMin
|
||||
song_difficulty_max: DifficultyModeOverrideMax
|
||||
|
||||
@@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": False,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
@@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 5,
|
||||
"additional_song_count": 34,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
@@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
|
||||
"allow_just_as_planned_dlc_songs": True,
|
||||
"starting_song_count": 8,
|
||||
"additional_song_count": 91,
|
||||
"additional_item_percentage": 80,
|
||||
"music_sheet_count_percentage": 20,
|
||||
"music_sheet_win_count_percentage": 90,
|
||||
},
|
||||
|
||||
+55
-39
@@ -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,
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
def test_all_difficulty_ranges(self) -> None:
|
||||
muse_dash_world = self.multiworld.worlds[1]
|
||||
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
|
||||
difficulty_choice = self.multiworld.song_difficulty_mode[1]
|
||||
difficulty_min = self.multiworld.song_difficulty_min[1]
|
||||
difficulty_max = self.multiworld.song_difficulty_max[1]
|
||||
difficulty_choice = muse_dash_world.options.song_difficulty_mode
|
||||
difficulty_min = muse_dash_world.options.song_difficulty_min
|
||||
difficulty_max = muse_dash_world.options.song_difficulty_max
|
||||
|
||||
def test_range(inputRange, lower, upper):
|
||||
self.assertEqual(inputRange[0], lower)
|
||||
@@ -66,9 +66,9 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
|
||||
song = muse_dash_world.md_collection.song_items[song_name]
|
||||
|
||||
# umpopoff is a one time weird song. Its currently the only song in the game
|
||||
# with non-standard difficulties and also doesn't have 3 or more difficulties.
|
||||
if song_name == 'umpopoff':
|
||||
# Some songs are weird and have less than the usual 3 difficulties.
|
||||
# So this override is to avoid failing on these songs.
|
||||
if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
|
||||
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
|
||||
f"Song '{song_name}' difficulty not set when it should be.")
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MuseDashTestBase(WorldTestBase):
|
||||
|
||||
@@ -38,7 +38,7 @@ class NoitaWorld(World):
|
||||
|
||||
web = NoitaWeb()
|
||||
|
||||
def generate_early(self):
|
||||
def generate_early(self) -> None:
|
||||
if not self.multiworld.get_player_name(self.player).isascii():
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class NoitaLocation(Location):
|
||||
class LocationData(NamedTuple):
|
||||
id: int
|
||||
flag: int = 0
|
||||
ltype: Optional[str] = "shop"
|
||||
ltype: str = "shop"
|
||||
|
||||
|
||||
class LocationFlag(IntEnum):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -115,8 +115,6 @@ class Overcooked2World(World):
|
||||
region,
|
||||
)
|
||||
|
||||
location.event = is_event
|
||||
|
||||
if priority:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
else:
|
||||
|
||||
@@ -98,7 +98,7 @@ LEGENDARY_NAMES = {
|
||||
"Registeel": "REGISTEEL",
|
||||
"Mew": "MEW",
|
||||
"Deoxys": "DEOXYS",
|
||||
"Ho-oh": "HO_OH",
|
||||
"Ho-Oh": "HO_OH",
|
||||
"Lugia": "LUGIA",
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -2877,7 +2877,7 @@
|
||||
"tags": ["Pokedex"]
|
||||
},
|
||||
"POKEDEX_REWARD_250": {
|
||||
"label": "Pokedex - Ho-oh",
|
||||
"label": "Pokedex - Ho-Oh",
|
||||
"tags": ["Pokedex"]
|
||||
},
|
||||
"POKEDEX_REWARD_251": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)),
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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,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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user