mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-20 12:23:26 -07:00
Merge remote-tracking branch 'Main/main' into rework_accessibility
# Conflicts: # worlds/oot/Rules.py
This commit is contained in:
@@ -149,7 +149,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
class WebWorld:
|
||||
"""Webhost integration"""
|
||||
|
||||
settings_page: Union[bool, str] = True
|
||||
options_page: Union[bool, str] = True
|
||||
"""display a settings page. Can be a link to a specific page or external tool."""
|
||||
|
||||
game_info_languages: List[str] = ['en']
|
||||
|
||||
@@ -89,9 +89,6 @@ components: List[Component] = [
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
# BizHawk
|
||||
Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT,
|
||||
file_identifier=SuffixIdentifier()),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
|
||||
@@ -13,7 +13,6 @@ import typing
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT = 43055
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class ConnectionStatus(enum.IntEnum):
|
||||
@@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum):
|
||||
CONNECTED = 3
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
class NotConnectedError(Exception):
|
||||
"""Raised when something tries to make a request to the connector script before a connection has been established"""
|
||||
pass
|
||||
@@ -51,6 +41,50 @@ class SyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
_lock: asyncio.Lock
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _send_message(self, message: str):
|
||||
async with self._lock:
|
||||
if self.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = self.streams
|
||||
writer.write(message.encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if self.connection_status == ConnectionStatus.TENTATIVE:
|
||||
self.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
return res.decode("utf-8")
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
|
||||
|
||||
async def connect(ctx: BizHawkContext) -> bool:
|
||||
"""Attempts to establish a connection with the connector script. Returns True if successful."""
|
||||
try:
|
||||
@@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None:
|
||||
|
||||
|
||||
async def get_script_version(ctx: BizHawkContext) -> int:
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write("VERSION".encode("ascii") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
version = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if version == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
return int(version.decode("ascii"))
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return int(await ctx._send_message("VERSION"))
|
||||
|
||||
|
||||
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write(json.dumps(req_list).encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if ctx.connection_status == ConnectionStatus.TENTATIVE:
|
||||
ctx.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
ret = json.loads(res.decode("utf-8"))
|
||||
for response in ret:
|
||||
if response["type"] == "ERROR":
|
||||
raise ConnectorError(response["err"])
|
||||
|
||||
return ret
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
|
||||
|
||||
async def ping(ctx: BizHawkContext) -> None:
|
||||
|
||||
@@ -16,12 +16,22 @@ else:
|
||||
BizHawkClientContext = object
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier())
|
||||
components.append(component)
|
||||
|
||||
|
||||
class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
# Register handler
|
||||
if "system" in namespace:
|
||||
systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
|
||||
if systems not in AutoBizHawkClientRegister.game_handlers:
|
||||
@@ -30,6 +40,19 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
if "game" in namespace:
|
||||
AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
|
||||
|
||||
# Update launcher component's suffixes
|
||||
if "patch_suffix" in namespace:
|
||||
if namespace["patch_suffix"] is not None:
|
||||
existing_identifier: SuffixIdentifier = component.file_identifier
|
||||
new_suffixes = [*existing_identifier.suffixes]
|
||||
|
||||
if type(namespace["patch_suffix"]) is str:
|
||||
new_suffixes.append(namespace["patch_suffix"])
|
||||
else:
|
||||
new_suffixes.extend(namespace["patch_suffix"])
|
||||
|
||||
component.file_identifier = SuffixIdentifier(*new_suffixes)
|
||||
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
@@ -45,11 +68,14 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
|
||||
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
system: ClassVar[Union[str, Tuple[str, ...]]]
|
||||
"""The system that the game this client is for runs on"""
|
||||
"""The system(s) that the game this client is for runs on"""
|
||||
|
||||
game: ClassVar[str]
|
||||
"""The game this client is for"""
|
||||
|
||||
patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
|
||||
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
|
||||
@@ -75,13 +101,3 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
|
||||
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||
pass
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
|
||||
if not any(component.script_name == "BizHawkClient" for component in components):
|
||||
components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier()))
|
||||
|
||||
@@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo
|
||||
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import traceback
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -12,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser,
|
||||
import Patch
|
||||
import Utils
|
||||
|
||||
from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \
|
||||
get_system, ping
|
||||
from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \
|
||||
get_script_version, get_system, ping
|
||||
from .client import BizHawkClient, AutoBizHawkClientRegister
|
||||
|
||||
|
||||
@@ -132,6 +133,8 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
except RequestFailedError as exc:
|
||||
logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
|
||||
continue
|
||||
except NotConnectedError:
|
||||
continue
|
||||
|
||||
# Get slot name and send `Connect`
|
||||
if ctx.server is not None and ctx.username is None:
|
||||
@@ -146,8 +149,24 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
|
||||
|
||||
async def _run_game(rom: str):
|
||||
import webbrowser
|
||||
webbrowser.open(rom)
|
||||
import os
|
||||
auto_start = Utils.get_settings().bizhawkclient_options.rom_start
|
||||
|
||||
if auto_start is True:
|
||||
emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
|
||||
subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
elif isinstance(auto_start, str):
|
||||
import shlex
|
||||
|
||||
subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
|
||||
cwd=Utils.local_path("."),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
|
||||
@@ -477,8 +477,6 @@ def create_inverted_regions(world, player):
|
||||
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def mark_dark_world_regions(world, player):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
|
||||
@@ -535,8 +535,6 @@ def set_up_take_anys(world, player):
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def get_pool_core(world, player: int):
|
||||
shuffle = world.shuffle[player]
|
||||
|
||||
@@ -102,7 +102,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
|
||||
'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"),
|
||||
'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
|
||||
'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
|
||||
'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
|
||||
'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
|
||||
'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"),
|
||||
|
||||
@@ -382,8 +382,6 @@ def create_regions(world, player):
|
||||
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
|
||||
]
|
||||
|
||||
world.initialize_regions()
|
||||
|
||||
|
||||
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
@@ -67,7 +67,7 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf
|
||||
|
||||
### Überprüfung deiner YAML-Datei
|
||||
|
||||
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite
|
||||
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/check) Seite
|
||||
tun.
|
||||
|
||||
## ein Einzelspielerspiel erstellen
|
||||
|
||||
@@ -82,7 +82,7 @@ debe tener al menos un valor mayor que cero, si no la generación fallará.
|
||||
### Verificando tu archivo YAML
|
||||
|
||||
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
||||
[YAML Validator](/mysterycheck).
|
||||
[YAML Validator](/check).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ chaque paramètre il faut au moins une option qui soit paramétrée sur un nombr
|
||||
### Vérifier son fichier YAML
|
||||
|
||||
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
||||
[Validateur de YAML](/mysterycheck).
|
||||
[Validateur de YAML](/check).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from ..AutoWorld import WebWorld, World
|
||||
|
||||
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
settings_page = "games/Sudoku/info/en"
|
||||
options_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
|
||||
@@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int):
|
||||
|
||||
entrance_map = {
|
||||
"Level 1": lambda state:
|
||||
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9),
|
||||
state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8),
|
||||
"Level 2": lambda state:
|
||||
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17),
|
||||
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16),
|
||||
"Level 3": lambda state:
|
||||
state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25),
|
||||
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24),
|
||||
"Level 4": lambda state:
|
||||
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33)
|
||||
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32)
|
||||
}
|
||||
|
||||
for x, region_name in enumerate(region_map):
|
||||
|
||||
@@ -108,7 +108,7 @@ class BumpStikWorld(World):
|
||||
item_pool += self._create_item_in_quantities(
|
||||
name, frequencies[i])
|
||||
|
||||
item_delta = len(location_table) - len(item_pool) - 1
|
||||
item_delta = len(location_table) - len(item_pool)
|
||||
if item_delta > 0:
|
||||
item_pool += self._create_item_in_quantities(
|
||||
"Score Bonus", item_delta)
|
||||
@@ -116,13 +116,16 @@ class BumpStikWorld(World):
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def set_rules(self):
|
||||
forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player),
|
||||
"Booster Bumper", self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item(
|
||||
self.create_item(self.get_filler_item_name()))
|
||||
|
||||
for x in range(1, 32):
|
||||
self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \
|
||||
lambda state, x = x: state.has("Treasure Bumper", self.player, x)
|
||||
for x in range(1, 5):
|
||||
self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \
|
||||
lambda state, x = x: state.has("Booster Bumper", self.player, x)
|
||||
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \
|
||||
lambda state: state.has("Hazard Bumper", self.player, 25)
|
||||
|
||||
self.multiworld.completion_condition[self.player] = \
|
||||
lambda state: state.has("Booster Bumper", self.player, 5) and \
|
||||
state.has("Treasure Bumper", self.player, 32)
|
||||
|
||||
|
||||
39
worlds/bumpstik/test/TestLogic.py
Normal file
39
worlds/bumpstik/test/TestLogic.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from . import BumpStikTestBase
|
||||
|
||||
|
||||
class TestRuleLogic(BumpStikTestBase):
|
||||
def testLogic(self):
|
||||
for x in range(1, 33):
|
||||
if x == 32:
|
||||
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
|
||||
self.collect(self.get_item_by_name("Treasure Bumper"))
|
||||
if x % 8 == 0:
|
||||
bb_count = round(x / 8)
|
||||
|
||||
if bb_count < 4:
|
||||
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}"))
|
||||
elif bb_count == 4:
|
||||
bb_count += 1
|
||||
|
||||
for y in range(self.count("Booster Bumper"), bb_count):
|
||||
self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"),
|
||||
f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs")
|
||||
if y < 4:
|
||||
self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"),
|
||||
f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs")
|
||||
self.collect(self.get_item_by_name("Booster Bumper"))
|
||||
|
||||
if x < 31:
|
||||
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}"))
|
||||
elif x == 31:
|
||||
self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points"))
|
||||
|
||||
if x < 32:
|
||||
self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"),
|
||||
f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs")
|
||||
elif x == 32:
|
||||
self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points"))
|
||||
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
self.collect(self.get_items_by_name("Hazard Bumper"))
|
||||
self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards"))
|
||||
5
worlds/bumpstik/test/__init__.py
Normal file
5
worlds/bumpstik/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
|
||||
|
||||
class BumpStikTestBase(WorldTestBase):
|
||||
game = "Bumper Stickers"
|
||||
@@ -50,7 +50,7 @@ them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from . import Options, data
|
||||
|
||||
class DLCQuestItem(Item):
|
||||
game: str = "DLCQuest"
|
||||
coins: int = 0
|
||||
coin_suffix: str = ""
|
||||
|
||||
|
||||
offset = 120_000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Entrance, MultiWorld, Region
|
||||
from . import Options
|
||||
@@ -9,318 +10,181 @@ DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Doub
|
||||
"Double Jump Behind the Tree", "The Forest", "Final Room"]
|
||||
|
||||
|
||||
def add_coin_freemium(region: Region, Coin: int, player: int):
|
||||
number_coin = f"{Coin} coins freemium"
|
||||
location_coin = f"{region.name} coins freemium"
|
||||
def add_coin_lfod(region: Region, coin: int, player: int):
|
||||
add_coin(region, coin, player, " coins freemium")
|
||||
|
||||
|
||||
def add_coin_dlcquest(region: Region, coin: int, player: int):
|
||||
add_coin(region, coin, player, " coins")
|
||||
|
||||
|
||||
def add_coin(region: Region, coin: int, player: int, suffix: str):
|
||||
number_coin = f"{coin}{suffix}"
|
||||
location_coin = f"{region.name}{suffix}"
|
||||
location = DLCQuestLocation(player, location_coin, None, region)
|
||||
region.locations.append(location)
|
||||
location.place_locked_item(create_event(player, number_coin))
|
||||
event = create_event(player, number_coin)
|
||||
event.coins = coin
|
||||
event.coin_suffix = suffix
|
||||
location.place_locked_item(event)
|
||||
|
||||
|
||||
def add_coin_dlcquest(region: Region, Coin: int, player: int):
|
||||
number_coin = f"{Coin} coins"
|
||||
location_coin = f"{region.name} coins"
|
||||
location = DLCQuestLocation(player, location_coin, None, region)
|
||||
region.locations.append(location)
|
||||
location.place_locked_item(create_event(player, number_coin))
|
||||
def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions):
|
||||
region_menu = Region("Menu", player, multiworld)
|
||||
has_campaign_basic = world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both
|
||||
has_campaign_lfod = world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both
|
||||
has_coinsanity = world_options.coinsanity == Options.CoinSanity.option_coin
|
||||
coin_bundle_size = world_options.coinbundlequantity.value
|
||||
has_item_shuffle = world_options.item_shuffle == Options.ItemShuffle.option_shuffled
|
||||
|
||||
multiworld.regions.append(region_menu)
|
||||
|
||||
create_regions_basic_campaign(has_campaign_basic, region_menu, has_item_shuffle, has_coinsanity, coin_bundle_size, player, multiworld)
|
||||
|
||||
create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu)
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
|
||||
Regmenu = Region("Menu", player, world)
|
||||
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
|
||||
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
|
||||
world.regions.append(Regmenu)
|
||||
def create_regions_basic_campaign(has_campaign_basic: bool, region_menu: Region, has_item_shuffle: bool, has_coinsanity: bool,
|
||||
coin_bundle_size: int, player: int, world: MultiWorld):
|
||||
if not has_campaign_basic:
|
||||
return
|
||||
|
||||
if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
region_menu.exits += [Entrance(player, "DLC Quest Basic", region_menu)]
|
||||
locations_move_right = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
|
||||
region_move_right = create_region_and_locations_basic("Move Right", locations_move_right, ["Moving"], player, world, 4)
|
||||
create_coinsanity_locations_dlc_quest(has_coinsanity, coin_bundle_size, player, region_move_right)
|
||||
locations_movement_pack = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"]
|
||||
locations_movement_pack += conditional_location(has_item_shuffle, "Sword")
|
||||
create_region_and_locations_basic("Movement Pack", locations_movement_pack, ["Tree", "Cloud"], player, world, 46)
|
||||
locations_behind_tree = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + conditional_location(has_item_shuffle, "Gun")
|
||||
create_region_and_locations_basic("Behind Tree", locations_behind_tree, ["Behind Tree Double Jump", "Forest Entrance"], player, world, 60)
|
||||
create_region_and_locations_basic("Psychological Warfare", ["West Cave Sheep"], ["Cloud Double Jump"], player, world, 100)
|
||||
locations_double_jump_left = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
|
||||
create_region_and_locations_basic("Double Jump Total Left", locations_double_jump_left, ["Cave Tree", "Cave Roof"], player, world, 50)
|
||||
create_region_and_locations_basic("Double Jump Total Left Cave", ["Top Hat Sheep"], [], player, world, 9)
|
||||
create_region_and_locations_basic("Double Jump Total Left Roof", ["North West Ceiling Sheep"], [], player, world, 10)
|
||||
locations_double_jump_left_ceiling = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
|
||||
create_region_and_locations_basic("Double Jump Behind Tree", locations_double_jump_left_ceiling, ["True Double Jump"], player, world, 89)
|
||||
create_region_and_locations_basic("True Double Jump Behind Tree", ["Double Jump Floating Sheep", "Cutscene Sheep"], [], player, world, 7)
|
||||
create_region_and_locations_basic("The Forest", ["Gun Pack", "Night Map Pack"], ["Behind Ogre", "Forest Double Jump"], player, world, 171)
|
||||
create_region_and_locations_basic("The Forest with double Jump", ["The Zombie Pack", "Forest Low Sheep"], ["Forest True Double Jump"], player, world, 76)
|
||||
create_region_and_locations_basic("The Forest with double Jump Part 2", ["Forest High Sheep"], [], player, world, 203)
|
||||
region_final_boss_room = create_region_and_locations_basic("The Final Boss Room", ["Finish the Fight Pack"], [], player, world)
|
||||
|
||||
Regmoveright = Region("Move Right", player, world, "Start of the basic game")
|
||||
Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
|
||||
Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)]
|
||||
Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
|
||||
loc_name in Locmoveright_name]
|
||||
add_coin_dlcquest(Regmoveright, 4, player)
|
||||
if World_Options.coinsanity == Options.CoinSanity.option_coin:
|
||||
coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
|
||||
for i in range(coin_bundle_needed):
|
||||
item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
|
||||
Regmoveright.locations += [
|
||||
DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
|
||||
if 825 % World_Options.coinbundlequantity != 0:
|
||||
Regmoveright.locations += [
|
||||
DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
|
||||
Regmoveright)]
|
||||
world.regions.append(Regmoveright)
|
||||
create_victory_event(region_final_boss_room, "Winning Basic", "Victory Basic", player)
|
||||
|
||||
Regmovpack = Region("Movement Pack", player, world)
|
||||
Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
|
||||
"Shepherd Sheep"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locmovpack_name += ["Sword"]
|
||||
Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
|
||||
Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name
|
||||
in Locmovpack_name]
|
||||
add_coin_dlcquest(Regmovpack, 46, player)
|
||||
world.regions.append(Regmovpack)
|
||||
connect_entrances_basic(player, world)
|
||||
|
||||
Regbtree = Region("Behind Tree", player, world)
|
||||
Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locbtree_name += ["Gun"]
|
||||
Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
|
||||
Entrance(player, "Forest Entrance", Regbtree)]
|
||||
Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in
|
||||
Locbtree_name]
|
||||
add_coin_dlcquest(Regbtree, 60, player)
|
||||
world.regions.append(Regbtree)
|
||||
|
||||
Regpsywarfare = Region("Psychological Warfare", player, world)
|
||||
Locpsywarfare_name = ["West Cave Sheep"]
|
||||
Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)]
|
||||
Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for
|
||||
loc_name in Locpsywarfare_name]
|
||||
add_coin_dlcquest(Regpsywarfare, 100, player)
|
||||
world.regions.append(Regpsywarfare)
|
||||
def create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu):
|
||||
if not has_campaign_lfod:
|
||||
return
|
||||
|
||||
Regdoubleleft = Region("Double Jump Total Left", player, world)
|
||||
Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
|
||||
Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for
|
||||
loc_name in
|
||||
Locdoubleleft_name]
|
||||
Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft),
|
||||
Entrance(player, "Cave Roof", Regdoubleleft)]
|
||||
add_coin_dlcquest(Regdoubleleft, 50, player)
|
||||
world.regions.append(Regdoubleleft)
|
||||
region_menu.exits += [Entrance(player, "Live Freemium or Die", region_menu)]
|
||||
locations_lfod_start = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
|
||||
"Nice Try", "Story is Important", "I Get That Reference!"] + conditional_location(has_item_shuffle, "Wooden Sword")
|
||||
region_lfod_start = create_region_and_locations_lfod("Freemium Start", locations_lfod_start, ["Vines"], player, multiworld, 50)
|
||||
create_coinsanity_locations_lfod(has_coinsanity, coin_bundle_size, player, region_lfod_start)
|
||||
locations_behind_vines = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + conditional_location(has_item_shuffle, "Pickaxe")
|
||||
create_region_and_locations_lfod("Behind the Vines", locations_behind_vines, ["Wall Jump Entrance"], player, multiworld, 95)
|
||||
locations_wall_jump = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
|
||||
create_region_and_locations_lfod("Wall Jump", locations_wall_jump, ["Harmless Plants", "Pickaxe Hard Cave"], player, multiworld, 150)
|
||||
create_region_and_locations_lfod("Fake Ending", ["Cut Content Pack", "Name Change Pack"], ["Name Change Entrance", "Cut Content Entrance"], player,
|
||||
multiworld)
|
||||
create_region_and_locations_lfod("Hard Cave", [], ["Hard Cave Wall Jump"], player, multiworld, 20)
|
||||
create_region_and_locations_lfod("Hard Cave Wall Jump", ["Increased HP Pack"], [], player, multiworld, 130)
|
||||
create_region_and_locations_lfod("Cut Content", conditional_location(has_item_shuffle, "Humble Indie Bindle"), [], player, multiworld, 200)
|
||||
create_region_and_locations_lfod("Name Change", conditional_location(has_item_shuffle, "Box of Various Supplies"), ["Behind Rocks"], player, multiworld)
|
||||
create_region_and_locations_lfod("Top Right", ["Season Pass", "High Definition Next Gen Pack"], ["Blizzard"], player, multiworld, 90)
|
||||
create_region_and_locations_lfod("Season", ["Remove Ads Pack", "Not Exactly Noble"], ["Boss Door"], player, multiworld, 154)
|
||||
region_final_boss = create_region_and_locations_lfod("Final Boss", ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"], [], player, multiworld)
|
||||
|
||||
Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world)
|
||||
Locdoubleleftcave_name = ["Top Hat Sheep"]
|
||||
Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave)
|
||||
for loc_name in Locdoubleleftcave_name]
|
||||
add_coin_dlcquest(Regdoubleleftcave, 9, player)
|
||||
world.regions.append(Regdoubleleftcave)
|
||||
create_victory_event(region_final_boss, "Winning Freemium", "Victory Freemium", player)
|
||||
|
||||
Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world)
|
||||
Locdoubleleftroof_name = ["North West Ceiling Sheep"]
|
||||
Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof)
|
||||
for loc_name in Locdoubleleftroof_name]
|
||||
add_coin_dlcquest(Regdoubleleftroof, 10, player)
|
||||
world.regions.append(Regdoubleleftroof)
|
||||
connect_entrances_lfod(multiworld, player)
|
||||
|
||||
Regdoubletree = Region("Double Jump Behind Tree", player, world)
|
||||
Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
|
||||
Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for
|
||||
loc_name in
|
||||
Locdoubletree_name]
|
||||
Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)]
|
||||
add_coin_dlcquest(Regdoubletree, 89, player)
|
||||
world.regions.append(Regdoubletree)
|
||||
|
||||
Regtruedoublejump = Region("True Double Jump Behind Tree", player, world)
|
||||
Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"]
|
||||
Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump)
|
||||
for loc_name in Loctruedoublejump_name]
|
||||
add_coin_dlcquest(Regtruedoublejump, 7, player)
|
||||
world.regions.append(Regtruedoublejump)
|
||||
def conditional_location(condition: bool, location: str) -> List[str]:
|
||||
return conditional_locations(condition, [location])
|
||||
|
||||
Regforest = Region("The Forest", player, world)
|
||||
Locforest_name = ["Gun Pack", "Night Map Pack"]
|
||||
Regforest.exits = [Entrance(player, "Behind Ogre", Regforest),
|
||||
Entrance(player, "Forest Double Jump", Regforest)]
|
||||
Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in
|
||||
Locforest_name]
|
||||
add_coin_dlcquest(Regforest, 171, player)
|
||||
world.regions.append(Regforest)
|
||||
|
||||
Regforestdoublejump = Region("The Forest whit double Jump", player, world)
|
||||
Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"]
|
||||
Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)]
|
||||
Regforestdoublejump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in
|
||||
Locforestdoublejump_name]
|
||||
add_coin_dlcquest(Regforestdoublejump, 76, player)
|
||||
world.regions.append(Regforestdoublejump)
|
||||
def conditional_locations(condition: bool, locations: List[str]) -> List[str]:
|
||||
return locations if condition else []
|
||||
|
||||
Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world)
|
||||
Locforesttruedoublejump_name = ["Forest High Sheep"]
|
||||
Regforesttruedoublejump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump)
|
||||
for loc_name in Locforesttruedoublejump_name]
|
||||
add_coin_dlcquest(Regforesttruedoublejump, 203, player)
|
||||
world.regions.append(Regforesttruedoublejump)
|
||||
|
||||
Regfinalroom = Region("The Final Boss Room", player, world)
|
||||
Locfinalroom_name = ["Finish the Fight Pack"]
|
||||
Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for
|
||||
loc_name in
|
||||
Locfinalroom_name]
|
||||
world.regions.append(Regfinalroom)
|
||||
def create_region_and_locations_basic(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins: int = 0) -> Region:
|
||||
return create_region_and_locations(region_name, locations, exits, player, multiworld, number_coins, 0)
|
||||
|
||||
loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player))
|
||||
world.get_region("The Final Boss Room", player).locations.append(loc_win)
|
||||
loc_win.place_locked_item(create_event(player, "Victory Basic"))
|
||||
|
||||
world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
|
||||
def create_region_and_locations_lfod(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins: int = 0) -> Region:
|
||||
return create_region_and_locations(region_name, locations, exits, player, multiworld, 0, number_coins)
|
||||
|
||||
world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
|
||||
|
||||
world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
|
||||
def create_region_and_locations(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
|
||||
number_coins_basic: int, number_coins_lfod: int) -> Region:
|
||||
region = Region(region_name, player, multiworld)
|
||||
region.exits = [Entrance(player, exit_name, region) for exit_name in exits]
|
||||
region.locations += [DLCQuestLocation(player, name, location_table[name], region) for name in locations]
|
||||
if number_coins_basic > 0:
|
||||
add_coin_dlcquest(region, number_coins_basic, player)
|
||||
if number_coins_lfod > 0:
|
||||
add_coin_lfod(region, number_coins_lfod, player)
|
||||
multiworld.regions.append(region)
|
||||
return region
|
||||
|
||||
world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
|
||||
|
||||
world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
|
||||
def create_victory_event(region_victory: Region, event_name: str, item_name: str, player: int):
|
||||
location_victory = DLCQuestLocation(player, event_name, None, region_victory)
|
||||
region_victory.locations.append(location_victory)
|
||||
location_victory.place_locked_item(create_event(player, item_name))
|
||||
|
||||
world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
|
||||
|
||||
world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
|
||||
def connect_entrances_basic(player, world):
|
||||
world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
|
||||
world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
|
||||
world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
|
||||
world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
|
||||
world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
|
||||
world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
|
||||
world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
|
||||
world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
|
||||
world.get_entrance("Behind Tree Double Jump", player).connect(world.get_region("Double Jump Behind Tree", player))
|
||||
world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
|
||||
world.get_entrance("Forest Double Jump", player).connect(world.get_region("The Forest with double Jump", player))
|
||||
world.get_entrance("Forest True Double Jump", player).connect(world.get_region("The Forest with double Jump Part 2", player))
|
||||
world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
|
||||
|
||||
world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
|
||||
|
||||
world.get_entrance("Behind Tree Double Jump", player).connect(
|
||||
world.get_region("Double Jump Behind Tree", player))
|
||||
def connect_entrances_lfod(multiworld, player):
|
||||
multiworld.get_entrance("Live Freemium or Die", player).connect(multiworld.get_region("Freemium Start", player))
|
||||
multiworld.get_entrance("Vines", player).connect(multiworld.get_region("Behind the Vines", player))
|
||||
multiworld.get_entrance("Wall Jump Entrance", player).connect(multiworld.get_region("Wall Jump", player))
|
||||
multiworld.get_entrance("Harmless Plants", player).connect(multiworld.get_region("Fake Ending", player))
|
||||
multiworld.get_entrance("Pickaxe Hard Cave", player).connect(multiworld.get_region("Hard Cave", player))
|
||||
multiworld.get_entrance("Hard Cave Wall Jump", player).connect(multiworld.get_region("Hard Cave Wall Jump", player))
|
||||
multiworld.get_entrance("Name Change Entrance", player).connect(multiworld.get_region("Name Change", player))
|
||||
multiworld.get_entrance("Cut Content Entrance", player).connect(multiworld.get_region("Cut Content", player))
|
||||
multiworld.get_entrance("Behind Rocks", player).connect(multiworld.get_region("Top Right", player))
|
||||
multiworld.get_entrance("Blizzard", player).connect(multiworld.get_region("Season", player))
|
||||
multiworld.get_entrance("Boss Door", player).connect(multiworld.get_region("Final Boss", player))
|
||||
|
||||
world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
|
||||
|
||||
world.get_entrance("Forest Double Jump", player).connect(
|
||||
world.get_region("The Forest whit double Jump", player))
|
||||
def create_coinsanity_locations_dlc_quest(has_coinsanity: bool, coin_bundle_size: int, player: int, region_move_right: Region):
|
||||
create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_move_right, 825, "DLC Quest")
|
||||
|
||||
world.get_entrance("Forest True Double Jump", player).connect(
|
||||
world.get_region("The Forest whit double Jump Part 2", player))
|
||||
|
||||
world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
|
||||
def create_coinsanity_locations_lfod(has_coinsanity: bool, coin_bundle_size: int, player: int, region_lfod_start: Region):
|
||||
create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_lfod_start, 889, "Live Freemium or Die")
|
||||
|
||||
if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
|
||||
== Options.Campaign.option_both):
|
||||
|
||||
Regfreemiumstart = Region("Freemium Start", player, world)
|
||||
Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
|
||||
"Nice Try", "Story is Important", "I Get That Reference!"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locfreemiumstart_name += ["Wooden Sword"]
|
||||
Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
|
||||
Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
|
||||
for loc_name in
|
||||
Locfreemiumstart_name]
|
||||
add_coin_freemium(Regfreemiumstart, 50, player)
|
||||
if World_Options.coinsanity == Options.CoinSanity.option_coin:
|
||||
coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
|
||||
for i in range(coin_bundle_needed):
|
||||
item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin"
|
||||
Regfreemiumstart.locations += [
|
||||
DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
|
||||
Regfreemiumstart)]
|
||||
if 889 % World_Options.coinbundlequantity != 0:
|
||||
Regfreemiumstart.locations += [
|
||||
DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
|
||||
location_table["Live Freemium or Die: 889 Coin"],
|
||||
Regfreemiumstart)]
|
||||
world.regions.append(Regfreemiumstart)
|
||||
def create_coinsanity_locations(has_coinsanity: bool, coin_bundle_size: int, player: int, region: Region, last_coin_number: int, campaign_prefix: str):
|
||||
if not has_coinsanity:
|
||||
return
|
||||
|
||||
Regbehindvine = Region("Behind the Vines", player, world)
|
||||
Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"]
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locbehindvine_name += ["Pickaxe"]
|
||||
Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
|
||||
Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for
|
||||
loc_name in Locbehindvine_name]
|
||||
add_coin_freemium(Regbehindvine, 95, player)
|
||||
world.regions.append(Regbehindvine)
|
||||
|
||||
Regwalljump = Region("Wall Jump", player, world)
|
||||
Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
|
||||
Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump),
|
||||
Entrance(player, "Pickaxe Hard Cave", Regwalljump)]
|
||||
Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for
|
||||
loc_name in Locwalljump_name]
|
||||
add_coin_freemium(Regwalljump, 150, player)
|
||||
world.regions.append(Regwalljump)
|
||||
|
||||
Regfakeending = Region("Fake Ending", player, world)
|
||||
Locfakeending_name = ["Cut Content Pack", "Name Change Pack"]
|
||||
Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending),
|
||||
Entrance(player, "Cut Content Entrance", Regfakeending)]
|
||||
Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for
|
||||
loc_name in Locfakeending_name]
|
||||
world.regions.append(Regfakeending)
|
||||
|
||||
Reghardcave = Region("Hard Cave", player, world)
|
||||
add_coin_freemium(Reghardcave, 20, player)
|
||||
Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)]
|
||||
world.regions.append(Reghardcave)
|
||||
|
||||
Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world)
|
||||
Lochardcavewalljump_name = ["Increased HP Pack"]
|
||||
Reghardcavewalljump.locations += [
|
||||
DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for
|
||||
loc_name in Lochardcavewalljump_name]
|
||||
add_coin_freemium(Reghardcavewalljump, 130, player)
|
||||
world.regions.append(Reghardcavewalljump)
|
||||
|
||||
Regcutcontent = Region("Cut Content", player, world)
|
||||
Loccutcontent_name = []
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Loccutcontent_name += ["Humble Indie Bindle"]
|
||||
Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
|
||||
loc_name in Loccutcontent_name]
|
||||
add_coin_freemium(Regcutcontent, 200, player)
|
||||
world.regions.append(Regcutcontent)
|
||||
|
||||
Regnamechange = Region("Name Change", player, world)
|
||||
Locnamechange_name = []
|
||||
if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
|
||||
Locnamechange_name += ["Box of Various Supplies"]
|
||||
Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
|
||||
Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for
|
||||
loc_name in Locnamechange_name]
|
||||
world.regions.append(Regnamechange)
|
||||
|
||||
Regtopright = Region("Top Right", player, world)
|
||||
Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"]
|
||||
Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)]
|
||||
Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for
|
||||
loc_name in Loctopright_name]
|
||||
add_coin_freemium(Regtopright, 90, player)
|
||||
world.regions.append(Regtopright)
|
||||
|
||||
Regseason = Region("Season", player, world)
|
||||
Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"]
|
||||
Regseason.exits = [Entrance(player, "Boss Door", Regseason)]
|
||||
Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for
|
||||
loc_name in Locseason_name]
|
||||
add_coin_freemium(Regseason, 154, player)
|
||||
world.regions.append(Regseason)
|
||||
|
||||
Regfinalboss = Region("Final Boss", player, world)
|
||||
Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"]
|
||||
Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for
|
||||
loc_name in Locfinalboss_name]
|
||||
world.regions.append(Regfinalboss)
|
||||
|
||||
loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player))
|
||||
world.get_region("Final Boss", player).locations.append(loc_wining)
|
||||
loc_wining.place_locked_item(create_event(player, "Victory Freemium"))
|
||||
|
||||
world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player))
|
||||
|
||||
world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player))
|
||||
|
||||
world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player))
|
||||
|
||||
world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player))
|
||||
|
||||
world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player))
|
||||
|
||||
world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player))
|
||||
|
||||
world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player))
|
||||
|
||||
world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player))
|
||||
|
||||
world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player))
|
||||
|
||||
world.get_entrance("Blizzard", player).connect(world.get_region("Season", player))
|
||||
|
||||
world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player))
|
||||
coin_bundle_needed = math.ceil(last_coin_number / coin_bundle_size)
|
||||
for i in range(1, coin_bundle_needed + 1):
|
||||
number_coins = min(last_coin_number, coin_bundle_size * i)
|
||||
item_coin = f"{campaign_prefix}: {number_coins} Coin"
|
||||
region.locations += [DLCQuestLocation(player, item_coin, location_table[item_coin], region)]
|
||||
|
||||
@@ -7,41 +7,25 @@ from . import Options
|
||||
from .Items import DLCQuestItem
|
||||
|
||||
|
||||
def create_event(player, event: str):
|
||||
def create_event(player, event: str) -> DLCQuestItem:
|
||||
return DLCQuestItem(event, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def has_enough_coin(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins", player] >= coin
|
||||
|
||||
|
||||
def has_enough_coin_freemium(player: int, coin: int):
|
||||
return lambda state: state.prog_items[" coins freemium", player] >= coin
|
||||
|
||||
|
||||
def set_rules(world, player, World_Options: Options.DLCQuestOptions):
|
||||
def has_enough_coin(player: int, coin: int):
|
||||
def has_coin(state, player: int, coins: int):
|
||||
coin_possessed = 0
|
||||
for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]:
|
||||
name_coin = f"{i} coins"
|
||||
if state.has(name_coin, player):
|
||||
coin_possessed += i
|
||||
|
||||
return coin_possessed >= coins
|
||||
|
||||
return lambda state: has_coin(state, player, coin)
|
||||
|
||||
def has_enough_coin_freemium(player: int, coin: int):
|
||||
def has_coin(state, player: int, coins: int):
|
||||
coin_possessed = 0
|
||||
for i in [20, 50, 90, 95, 130, 150, 154, 200]:
|
||||
name_coin = f"{i} coins freemium"
|
||||
if state.has(name_coin, player):
|
||||
coin_possessed += i
|
||||
|
||||
return coin_possessed >= coins
|
||||
|
||||
return lambda state: has_coin(state, player, coin)
|
||||
|
||||
set_basic_rules(World_Options, has_enough_coin, player, world)
|
||||
set_lfod_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
set_basic_rules(World_Options, player, world)
|
||||
set_lfod_rules(World_Options, player, world)
|
||||
set_completion_condition(World_Options, player, world)
|
||||
|
||||
|
||||
def set_basic_rules(World_Options, has_enough_coin, player, world):
|
||||
def set_basic_rules(World_Options, player, world):
|
||||
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
|
||||
return
|
||||
set_basic_entrance_rules(player, world)
|
||||
@@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world):
|
||||
set_basic_shuffled_items_rules(World_Options, player, world)
|
||||
set_double_jump_glitchless_rules(World_Options, player, world)
|
||||
set_easy_double_jump_glitch_rules(World_Options, player, world)
|
||||
self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world)
|
||||
set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world)
|
||||
self_basic_coinsanity_funded_purchase_rules(World_Options, player, world)
|
||||
set_basic_self_funded_purchase_rules(World_Options, player, world)
|
||||
self_basic_win_condition(World_Options, player, world)
|
||||
|
||||
|
||||
@@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
|
||||
lambda state: state.has("Double Jump Pack", player))
|
||||
|
||||
|
||||
def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
|
||||
def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_coin:
|
||||
return
|
||||
number_of_bundle = math.floor(825 / World_Options.coinbundlequantity)
|
||||
@@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin,
|
||||
math.ceil(5 / World_Options.coinbundlequantity)))
|
||||
|
||||
|
||||
def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
|
||||
def set_basic_self_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_none:
|
||||
return
|
||||
set_rule(world.get_location("Movement Pack", player),
|
||||
@@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world):
|
||||
player))
|
||||
|
||||
|
||||
def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
|
||||
def set_lfod_rules(World_Options, player, world):
|
||||
if World_Options.campaign == Options.Campaign.option_basic:
|
||||
return
|
||||
set_lfod_entrance_rules(player, world)
|
||||
set_boss_door_requirements_rules(player, world)
|
||||
set_lfod_self_obtained_items_rules(World_Options, player, world)
|
||||
set_lfod_shuffled_items_rules(World_Options, player, world)
|
||||
self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world)
|
||||
set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
|
||||
|
||||
|
||||
@@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
|
||||
lambda state: state.can_reach("Cut Content", 'region', player))
|
||||
|
||||
|
||||
def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
|
||||
def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world):
|
||||
if World_Options.coinsanity != Options.CoinSanity.option_coin:
|
||||
return
|
||||
number_of_bundle = math.floor(889 / World_Options.coinbundlequantity)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Union
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from BaseClasses import Tutorial, CollectionState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Options
|
||||
from .Items import DLCQuestItem, ItemData, create_items, item_table
|
||||
@@ -71,7 +71,6 @@ class DLCqworld(World):
|
||||
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
|
||||
self.multiworld.push_precollected(self.create_item("Movement Pack"))
|
||||
|
||||
|
||||
def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem:
|
||||
if isinstance(item, str):
|
||||
item = item_table[item]
|
||||
@@ -87,3 +86,19 @@ class DLCqworld(World):
|
||||
"seed": self.random.randrange(99999999)
|
||||
})
|
||||
return options_dict
|
||||
|
||||
def collect(self, state: CollectionState, item: DLCQuestItem) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] += item.coins
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
suffix = item.coin_suffix
|
||||
if suffix:
|
||||
state.prog_items[suffix, self.player] -= item.coins
|
||||
return change
|
||||
|
||||
130
worlds/dlcquest/test/TestItemShuffle.py
Normal file
130
worlds/dlcquest/test/TestItemShuffle.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from . import DLCQuestTestBase
|
||||
from .. import Options
|
||||
|
||||
sword = "Sword"
|
||||
gun = "Gun"
|
||||
wooden_sword = "Wooden Sword"
|
||||
pickaxe = "Pickaxe"
|
||||
humble_bindle = "Humble Indie Bindle"
|
||||
box_supplies = "Box of Various Supplies"
|
||||
items = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies]
|
||||
|
||||
important_pack = "Incredibly Important Pack"
|
||||
|
||||
|
||||
class TestItemShuffle(DLCQuestTestBase):
|
||||
options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_shuffled,
|
||||
Options.Campaign.internal_name: Options.Campaign.option_both}
|
||||
|
||||
def test_items_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
for item in items:
|
||||
with self.subTest(f"{item}"):
|
||||
self.assertIn(item, item_names)
|
||||
|
||||
def test_item_locations_in_pool(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
for item_location in items:
|
||||
with self.subTest(f"{item_location}"):
|
||||
self.assertIn(item_location, location_names)
|
||||
|
||||
def test_sword_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(sword))
|
||||
movement_pack = self.multiworld.create_item("Movement Pack", self.player)
|
||||
self.collect(movement_pack)
|
||||
self.assertFalse(self.can_reach_location(sword))
|
||||
time_pack = self.multiworld.create_item("Time is Money Pack", self.player)
|
||||
self.collect(time_pack)
|
||||
self.assertTrue(self.can_reach_location(sword))
|
||||
|
||||
def test_gun_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
movement_pack = self.multiworld.create_item("Movement Pack", self.player)
|
||||
self.collect(movement_pack)
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
sword_item = self.multiworld.create_item(sword, self.player)
|
||||
self.collect(sword_item)
|
||||
self.assertFalse(self.can_reach_location(gun))
|
||||
gun_pack = self.multiworld.create_item("Gun Pack", self.player)
|
||||
self.collect(gun_pack)
|
||||
self.assertTrue(self.can_reach_location(gun))
|
||||
|
||||
def test_wooden_sword_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(wooden_sword))
|
||||
important_pack_item = self.multiworld.create_item(important_pack, self.player)
|
||||
self.collect(important_pack_item)
|
||||
self.assertTrue(self.can_reach_location(wooden_sword))
|
||||
|
||||
def test_bindle_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
|
||||
self.collect(cut_content_pack)
|
||||
self.assertFalse(self.can_reach_location(humble_bindle))
|
||||
box_supplies_item = self.multiworld.create_item(box_supplies, self.player)
|
||||
self.collect(box_supplies_item)
|
||||
self.assertTrue(self.can_reach_location(humble_bindle))
|
||||
|
||||
def test_box_supplies_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(box_supplies))
|
||||
cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
|
||||
self.collect(cut_content_pack)
|
||||
self.assertTrue(self.can_reach_location(box_supplies))
|
||||
|
||||
def test_pickaxe_location_has_correct_rules(self):
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
|
||||
self.collect(wooden_sword_item)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
|
||||
self.collect(plants_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
|
||||
self.collect(wall_jump_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
|
||||
self.collect(name_change_pack)
|
||||
self.assertFalse(self.can_reach_location(pickaxe))
|
||||
bindle_item = self.multiworld.create_item("Humble Indie Bindle", self.player)
|
||||
self.collect(bindle_item)
|
||||
self.assertTrue(self.can_reach_location(pickaxe))
|
||||
|
||||
|
||||
class TestNoItemShuffle(DLCQuestTestBase):
|
||||
options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_disabled,
|
||||
Options.Campaign.internal_name: Options.Campaign.option_both}
|
||||
|
||||
def test_items_not_in_pool(self):
|
||||
item_names = {item.name for item in self.multiworld.get_items()}
|
||||
for item in items:
|
||||
with self.subTest(f"{item}"):
|
||||
self.assertNotIn(item, item_names)
|
||||
|
||||
def test_item_locations_not_in_pool(self):
|
||||
location_names = {location.name for location in self.multiworld.get_locations()}
|
||||
for item_location in items:
|
||||
with self.subTest(f"{item_location}"):
|
||||
self.assertNotIn(item_location, location_names)
|
||||
87
worlds/dlcquest/test/TestOptionsLong.py
Normal file
87
worlds/dlcquest/test/TestOptionsLong.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import SpecialRange
|
||||
from .option_names import options_to_include
|
||||
from .checks.world_checks import assert_can_win, assert_same_number_items_locations
|
||||
from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
|
||||
from ... import AutoWorldRegister
|
||||
|
||||
|
||||
def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
assert_can_win(tester, multiworld)
|
||||
assert_same_number_items_locations(tester, multiworld)
|
||||
|
||||
|
||||
def get_option_choices(option) -> Dict[str, int]:
|
||||
if issubclass(option, SpecialRange):
|
||||
return option.special_range_names
|
||||
elif option.options:
|
||||
return option.options
|
||||
return {}
|
||||
|
||||
|
||||
class TestGenerateDynamicOptions(DLCQuestTestBase):
|
||||
def test_given_option_pair_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_truple_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
for option3_index in range(option2_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option3 = options_to_include[option3_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
option3_choices = get_option_choices(option3)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
for key3 in option3_choices:
|
||||
with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2],
|
||||
option3.internal_name: option3_choices[key3]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
|
||||
def test_given_option_quartet_when_generate_then_basic_checks(self):
|
||||
num_options = len(options_to_include)
|
||||
for option1_index in range(0, num_options):
|
||||
for option2_index in range(option1_index + 1, num_options):
|
||||
for option3_index in range(option2_index + 1, num_options):
|
||||
for option4_index in range(option3_index + 1, num_options):
|
||||
option1 = options_to_include[option1_index]
|
||||
option2 = options_to_include[option2_index]
|
||||
option3 = options_to_include[option3_index]
|
||||
option4 = options_to_include[option4_index]
|
||||
option1_choices = get_option_choices(option1)
|
||||
option2_choices = get_option_choices(option2)
|
||||
option3_choices = get_option_choices(option3)
|
||||
option4_choices = get_option_choices(option4)
|
||||
for key1 in option1_choices:
|
||||
for key2 in option2_choices:
|
||||
for key3 in option3_choices:
|
||||
for key4 in option4_choices:
|
||||
with self.subTest(
|
||||
f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}, {option4.internal_name}: {key4}"):
|
||||
choices = {option1.internal_name: option1_choices[key1],
|
||||
option2.internal_name: option2_choices[key2],
|
||||
option3.internal_name: option3_choices[key3],
|
||||
option4.internal_name: option4_choices[key4]}
|
||||
multiworld = setup_dlc_quest_solo_multiworld(choices)
|
||||
basic_checks(self, multiworld)
|
||||
53
worlds/dlcquest/test/__init__.py
Normal file
53
worlds/dlcquest/test/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from typing import Dict, FrozenSet, Tuple, Any
|
||||
from argparse import Namespace
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.TestBase import WorldTestBase
|
||||
from .. import DLCqworld
|
||||
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
|
||||
class DLCQuestTestBase(WorldTestBase):
|
||||
game = "DLCQuest"
|
||||
world: DLCqworld
|
||||
player: ClassVar[int] = 1
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
super().world_setup(*args, **kwargs)
|
||||
if self.constructed:
|
||||
self.world = self.multiworld.worlds[self.player] # noqa
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# world_setup is overridden, so it'd always run default tests when importing DLCQuestTestBase
|
||||
is_not_dlc_test = type(self) is not DLCQuestTestBase
|
||||
should_run_default_tests = is_not_dlc_test and super().run_default_tests
|
||||
return should_run_default_tests
|
||||
|
||||
|
||||
def setup_dlc_quest_solo_multiworld(test_options=None, seed=None, _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: #noqa
|
||||
if test_options is None:
|
||||
test_options = {}
|
||||
|
||||
# Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds
|
||||
frozen_options = frozenset(test_options.items()).union({seed})
|
||||
if frozen_options in _cache:
|
||||
return _cache[frozen_options]
|
||||
|
||||
multiworld = setup_base_solo_multiworld(DLCqworld, ())
|
||||
multiworld.set_seed(seed)
|
||||
# print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
|
||||
args = Namespace()
|
||||
for name, option in DLCqworld.options_dataclass.type_hints.items():
|
||||
value = option(test_options[name]) if name in test_options else option.from_any(option.default)
|
||||
setattr(args, name, {1: value})
|
||||
multiworld.set_options(args)
|
||||
for step in gen_steps:
|
||||
call_all(multiworld, step)
|
||||
|
||||
_cache[frozen_options] = multiworld
|
||||
|
||||
return multiworld
|
||||
0
worlds/dlcquest/test/checks/__init__.py
Normal file
0
worlds/dlcquest/test/checks/__init__.py
Normal file
42
worlds/dlcquest/test/checks/world_checks.py
Normal file
42
worlds/dlcquest/test/checks/world_checks.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import MultiWorld, ItemClassification
|
||||
from .. import DLCQuestTestBase
|
||||
from ... import Options
|
||||
|
||||
|
||||
def get_all_item_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [item.name for item in multiworld.itempool]
|
||||
|
||||
|
||||
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
|
||||
return [location.name for location in multiworld.get_locations() if not location.event]
|
||||
|
||||
|
||||
def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
campaign = multiworld.campaign[1]
|
||||
all_items = [item.name for item in multiworld.get_items()]
|
||||
if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
|
||||
tester.assertIn("Victory Basic", all_items)
|
||||
if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
|
||||
tester.assertIn("Victory Freemium", all_items)
|
||||
|
||||
|
||||
def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
for item in multiworld.get_items():
|
||||
multiworld.state.collect(item)
|
||||
campaign = multiworld.campaign[1]
|
||||
if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
|
||||
tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state))
|
||||
if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
|
||||
tester.assertTrue(multiworld.find_item("Victory Freemium", 1).can_reach(multiworld.state))
|
||||
|
||||
|
||||
def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
|
||||
assert_victory_exists(tester, multiworld)
|
||||
collect_all_then_assert_can_win(tester, 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]
|
||||
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
|
||||
5
worlds/dlcquest/test/option_names.py
Normal file
5
worlds/dlcquest/test/option_names.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .. import DLCqworld
|
||||
|
||||
options_to_exclude = ["progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
|
||||
options_to_include = [option for option_name, option in DLCqworld.options_dataclass.type_hints.items()
|
||||
if option_name not in options_to_exclude]
|
||||
@@ -31,7 +31,7 @@ them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/pl
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
Validator page: [Yaml Validation Page](/mysterycheck)
|
||||
Validator page: [Yaml Validation Page](/check)
|
||||
|
||||
## Connecting to Someone Else's Factorio Game
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class FF1Settings(settings.Group):
|
||||
|
||||
|
||||
class FF1Web(WebWorld):
|
||||
settings_page = "https://finalfantasyrandomizer.com/"
|
||||
options_page = "https://finalfantasyrandomizer.com/"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
|
||||
@@ -91,7 +91,7 @@ class FF1World(World):
|
||||
def set_rules(self):
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(CHAOS_TERMINATED_EVENT, self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
def create_items(self):
|
||||
items = get_options(self.multiworld, 'items', self.player)
|
||||
if FF1_BRIDGE in items.keys():
|
||||
self._place_locked_item_in_sphere0(FF1_BRIDGE)
|
||||
|
||||
@@ -108,7 +108,9 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
* `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically
|
||||
but may not be able to access all locations or acquire all items. A good example of this is having a big key in
|
||||
the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon.
|
||||
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
|
||||
* `progression_balancing` is a system the Archipelago generator uses to try and reduce
|
||||
["BK mode"](/glossary/en/#burger-king-/-bk-mode)
|
||||
as much as possible.
|
||||
This primarily involves moving necessary progression items into earlier logic spheres to make the games more
|
||||
accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by
|
||||
default. This number represents a percentage of the furthest progressible player.
|
||||
@@ -130,7 +132,7 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
there without using any hint points.
|
||||
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
|
||||
item which isn't necessary for progression to go in these locations.
|
||||
* `priority_locations` is the inverse of `exlcude_locations`, forcing a progression item in the defined locations.
|
||||
* `priority_locations` is the inverse of `exclude_locations`, forcing a progression item in the defined locations.
|
||||
* `item_links` allows players to link their items into a group with the same item link name and game. The items declared
|
||||
in `item_pool` get combined and when an item is found for the group, all players in the group receive it. Item links
|
||||
can also have local and non local items, forcing the items to either be placed within the worlds of the group or in
|
||||
|
||||
@@ -40,7 +40,7 @@ game you will be playing as well as the settings you would like for that game.
|
||||
|
||||
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
|
||||
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
|
||||
[YAML Validation Page](/mysterycheck)
|
||||
[YAML Validation Page](/check)
|
||||
|
||||
### Creating a YAML
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# Hollow Knight for Archipelago Setup Guide
|
||||
|
||||
## Required Software
|
||||
* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
|
||||
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
||||
* A legal copy of Hollow Knight.
|
||||
|
||||
## Installing the Archipelago Mod using Scarab+
|
||||
1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
|
||||
## Installing the Archipelago Mod using Lumafly
|
||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||
2. Click the "Install" button near the "Archipelago" mod entry.
|
||||
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
|
||||
3. Launch the game, you're all set!
|
||||
|
||||
### What to do if Scarab+ fails to find your XBox Game Pass installation directory
|
||||
### What to do if Lumafly fails to find your XBox Game Pass installation directory
|
||||
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
5. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 4.
|
||||
5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4.
|
||||
|
||||
#### Alternative Method:
|
||||
1. Click on your profile then "Settings".
|
||||
2. Go to the "General" tab and select "CHANGE FOLDER".
|
||||
3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
|
||||
4. Run Scarab+ as an administrator and, when it asks you for the path, paste what you copied in step 3.
|
||||
4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3.
|
||||
|
||||
Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
|
||||
|
||||
|
||||
@@ -1,91 +1,128 @@
|
||||
from worlds.generic.Rules import add_rule
|
||||
from ..AutoWorld import LogicMixin
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
class Hylics2Logic(LogicMixin):
|
||||
def air_dash(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PNEUMATOPHORE", player)
|
||||
|
||||
def _hylics2_can_air_dash(self, player):
|
||||
return self.has("PNEUMATOPHORE", player)
|
||||
|
||||
def _hylics2_has_airship(self, player):
|
||||
return self.has("DOCK KEY", player)
|
||||
def airship(state: CollectionState, player: int) -> bool:
|
||||
return state.has("DOCK KEY", player)
|
||||
|
||||
def _hylics2_has_jail_key(self, player):
|
||||
return self.has("JAIL KEY", player)
|
||||
|
||||
def _hylics2_has_paddle(self, player):
|
||||
return self.has("PADDLE", player)
|
||||
def jail_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("JAIL KEY", player)
|
||||
|
||||
def _hylics2_has_worm_room_key(self, player):
|
||||
return self.has("WORM ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_bridge_key(self, player):
|
||||
return self.has("BRIDGE KEY", player)
|
||||
def paddle(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PADDLE", player)
|
||||
|
||||
def _hylics2_has_upper_chamber_key(self, player):
|
||||
return self.has("UPPER CHAMBER KEY", player)
|
||||
|
||||
def _hylics2_has_vessel_room_key(self, player):
|
||||
return self.has("VESSEL ROOM KEY", player)
|
||||
def worm_room_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("WORM ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_house_key(self, player):
|
||||
return self.has("HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_cave_key(self, player):
|
||||
return self.has("CAVE KEY", player)
|
||||
def bridge_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("BRIDGE KEY", player)
|
||||
|
||||
def _hylics2_has_skull_bomb(self, player):
|
||||
return self.has("SKULL BOMB", player)
|
||||
|
||||
def _hylics2_has_tower_key(self, player):
|
||||
return self.has("TOWER KEY", player)
|
||||
def upper_chamber_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("UPPER CHAMBER KEY", player)
|
||||
|
||||
def _hylics2_has_deep_key(self, player):
|
||||
return self.has("DEEP KEY", player)
|
||||
|
||||
def _hylics2_has_upper_house_key(self, player):
|
||||
return self.has("UPPER HOUSE KEY", player)
|
||||
def vessel_room_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("VESSEL ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_clicker(self, player):
|
||||
return self.has("CLICKER", player)
|
||||
|
||||
def _hylics2_has_tokens(self, player):
|
||||
return self.has("SAGE TOKEN", player, 3)
|
||||
def house_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_charge_up(self, player):
|
||||
return self.has("CHARGE UP", player)
|
||||
|
||||
def _hylics2_has_cup(self, player):
|
||||
return self.has("PAPER CUP", player, 1)
|
||||
def cave_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CAVE KEY", player)
|
||||
|
||||
def _hylics2_has_1_member(self, player):
|
||||
return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_has_2_members(self, player):
|
||||
return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
|
||||
(self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
|
||||
(self.has("Dedusmuln", player) and self.has("Somsnosa", player))
|
||||
def skull_bomb(state: CollectionState, player: int) -> bool:
|
||||
return state.has("SKULL BOMB", player)
|
||||
|
||||
def _hylics2_has_3_members(self, player):
|
||||
return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_enter_arcade2(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
|
||||
def tower_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("TOWER KEY", player)
|
||||
|
||||
def _hylics2_enter_wormpod(self, player):
|
||||
return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
|
||||
def _hylics2_enter_sageship(self, player):
|
||||
return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
def deep_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("DEEP KEY", player)
|
||||
|
||||
def _hylics2_enter_foglast(self, player):
|
||||
return self._hylics2_enter_wormpod(player)
|
||||
|
||||
def _hylics2_enter_hylemxylem(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_enter_foglast(player) and\
|
||||
self._hylics2_has_bridge_key(player)
|
||||
def upper_house_key(state: CollectionState, player: int) -> bool:
|
||||
return state.has("UPPER HOUSE KEY", player)
|
||||
|
||||
|
||||
def clicker(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CLICKER", player)
|
||||
|
||||
|
||||
def all_tokens(state: CollectionState, player: int) -> bool:
|
||||
return state.has("SAGE TOKEN", player, 3)
|
||||
|
||||
|
||||
def charge_up(state: CollectionState, player: int) -> bool:
|
||||
return state.has("CHARGE UP", player)
|
||||
|
||||
|
||||
def paper_cup(state: CollectionState, player: int) -> bool:
|
||||
return state.has("PAPER CUP", player)
|
||||
|
||||
|
||||
def party_1(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
|
||||
|
||||
|
||||
def party_2(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
state.has_all({"Pongorma", "Dedusmuln"}, player)
|
||||
or state.has_all({"Pongorma", "Somsnosa"}, player)
|
||||
or state.has_all({"Dedusmuln", "Somsnosa"}, player)
|
||||
)
|
||||
|
||||
|
||||
def party_3(state: CollectionState, player: int) -> bool:
|
||||
return state.has_all({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
|
||||
|
||||
|
||||
def enter_arcade2(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
air_dash(state, player)
|
||||
and airship(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_wormpod(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
airship(state, player)
|
||||
and worm_room_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_sageship(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
skull_bomb(state, player)
|
||||
and airship(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
|
||||
|
||||
def enter_foglast(state: CollectionState, player: int) -> bool:
|
||||
return enter_wormpod(state, player)
|
||||
|
||||
|
||||
def enter_hylemxylem(state: CollectionState, player: int) -> bool:
|
||||
return (
|
||||
air_dash(state, player)
|
||||
and enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
)
|
||||
|
||||
|
||||
def set_rules(hylics2world):
|
||||
@@ -94,342 +131,439 @@ def set_rules(hylics2world):
|
||||
|
||||
# Afterlife
|
||||
add_rule(world.get_location("Afterlife: TV", player),
|
||||
lambda state: state._hylics2_has_cave_key(player))
|
||||
lambda state: cave_key(state, player))
|
||||
|
||||
# New Muldul
|
||||
add_rule(world.get_location("New Muldul: Underground Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("New Muldul: TV", player),
|
||||
lambda state: state._hylics2_has_house_key(player))
|
||||
lambda state: house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Pot above Vault", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# New Muldul Vault
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
|
||||
(state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
|
||||
state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: (
|
||||
(
|
||||
(
|
||||
jail_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
and (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
)
|
||||
)
|
||||
or enter_hylemxylem(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
|
||||
(state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
|
||||
state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: (
|
||||
(
|
||||
(
|
||||
jail_key(state, player)
|
||||
and paddle(state, player)
|
||||
)
|
||||
and (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
)
|
||||
)
|
||||
or enter_hylemxylem(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
lambda state: enter_hylemxylem(state, player))
|
||||
|
||||
# Viewax's Edifice
|
||||
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
|
||||
lambda state: (
|
||||
paddle(state, player)
|
||||
and tower_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: TV", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
|
||||
lambda state: (
|
||||
paddle(state, player)
|
||||
and jail_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# Arcade 1
|
||||
add_rule(world.get_location("Arcade 1: Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Coin Dash", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Bed Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
|
||||
# Airship
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_worm_room_key(player))
|
||||
lambda state: worm_room_key(state, player))
|
||||
|
||||
# Foglast
|
||||
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Shielded Key", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and clicker(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Buy Clicker", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Shielded Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Cave Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Foglast: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
air_dash(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
|
||||
# Drill Castle
|
||||
add_rule(world.get_location("Drill Castle: Island Banana", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: Island Pot", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Drill Castle: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
|
||||
# Sage Labyrinth
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
lambda state: deep_key(state, player))
|
||||
|
||||
# Sage Airship
|
||||
add_rule(world.get_location("Sage Airship: TV", player),
|
||||
lambda state: state._hylics2_has_tokens(player))
|
||||
lambda state: all_tokens(state, player))
|
||||
|
||||
# Hylemxylem
|
||||
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
|
||||
# extra rules if Extra Items in Logic is enabled
|
||||
if world.extra_items_in_logic[player]:
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player))
|
||||
add_rule(i, lambda state: charge_up(state, player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
|
||||
state._hylics2_has_worm_room_key(player))
|
||||
add_rule(i, lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
and worm_room_key(state, player)
|
||||
))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
add_rule(i, lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
))
|
||||
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
lambda state: (
|
||||
charge_up(state, player)
|
||||
and paper_cup(state, player)
|
||||
))
|
||||
|
||||
# extra rules if Shuffle Party Members is enabled
|
||||
if world.party_shuffle[player]:
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
|
||||
(state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
|
||||
add_rule(i, lambda state: (
|
||||
party_3(state, player)
|
||||
or (
|
||||
party_2(state, player)
|
||||
and jail_key(state, player)
|
||||
)
|
||||
))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(i, lambda state: party_3(state, player))
|
||||
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
lambda state: party_2(state, player))
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
|
||||
# extra rules if Shuffle Red Medallions is enabled
|
||||
if world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
lambda state: upper_house_key(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
lambda state: (
|
||||
enter_foglast(state, player)
|
||||
and bridge_key(state, player)
|
||||
))
|
||||
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
|
||||
lambda state: state._hylics2_has_bridge_key(player))
|
||||
lambda state: bridge_key(state, player))
|
||||
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
lambda state: air_dash(state, player))
|
||||
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
lambda state: paddle(state, player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
lambda state: upper_chamber_key(state, player))
|
||||
|
||||
# extra rules is Shuffle Red Medallions and Party Shuffle are enabled
|
||||
# extra rules if Shuffle Red Medallions and Party Shuffle are enabled
|
||||
if world.party_shuffle[player] and world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
lambda state: party_3(state, player))
|
||||
|
||||
# entrances
|
||||
for i in world.get_region("Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
|
||||
add_rule(i, lambda state: (
|
||||
airship(state, player)
|
||||
and air_dash(state, player)
|
||||
))
|
||||
for i in world.get_region("Worm Pod", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
|
||||
add_rule(i, lambda state: enter_wormpod(state, player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_foglast(player))
|
||||
add_rule(i, lambda state: enter_foglast(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
|
||||
add_rule(i, lambda state: skull_bomb(state, player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_sageship(player))
|
||||
add_rule(i, lambda state: enter_sageship(state, player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
add_rule(i, lambda state: enter_hylemxylem(state, player))
|
||||
|
||||
# random start logic (default)
|
||||
if ((not world.random_start[player]) or \
|
||||
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
|
||||
# entrances
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
and airship(state, player)
|
||||
))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (Viewax's Edifice)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: (
|
||||
air_dash(state, player)
|
||||
or airship(state, player)
|
||||
))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (TV Island)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
|
||||
# random start logic (Shield Facility)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
add_rule(i, lambda state: airship(state, player))
|
||||
@@ -130,11 +130,11 @@ class Hylics2World(World):
|
||||
tvs = list(Locations.tv_location_table.items())
|
||||
|
||||
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
|
||||
# placed at Sage Airship: TV
|
||||
# placed at Sage Airship: TV or Foglast: TV
|
||||
if self.multiworld.extra_items_in_logic[self.player]:
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
gest = gestures.index((200681, Items.gesture_item_table[200681]))
|
||||
while tv[1]["name"] == "Sage Airship: TV":
|
||||
while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
|
||||
@@ -146,7 +146,7 @@ class Hylics2World(World):
|
||||
gest = self.multiworld.random.choice(gestures)
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
|
||||
gestures.remove(gest)
|
||||
tvs.remove(tv)
|
||||
|
||||
@@ -232,8 +232,10 @@ class Hylics2World(World):
|
||||
# create location for beating the game and place Victory event there
|
||||
loc = Location(self.player, "Defeat Gibby", None, self.multiworld.get_region("Hylemxylem", self.player))
|
||||
loc.place_locked_item(self.create_event("Victory"))
|
||||
set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
|
||||
and state._hylics2_has_vessel_room_key(self.player))
|
||||
set_rule(loc, lambda state: (
|
||||
state.has("UPPER CHAMBER KEY", self.player)
|
||||
and state.has("VESSEL ROOM KEY", self.player)
|
||||
))
|
||||
self.multiworld.get_region("Hylemxylem", self.player).locations.append(loc)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ your personal settings and export a config file from them.
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
[YAML Validator](/check) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ from enum import auto, Enum
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from . import Locations
|
||||
|
||||
start_id: int = Locations.start_id
|
||||
start_id: int = 0xAC0000
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
@@ -500,7 +499,7 @@ l2ac_item_table: Dict[str, ItemData] = {
|
||||
# 0x01C8: "Key28"
|
||||
# 0x01C9: "Key29"
|
||||
# 0x01CA: "AP item" # replaces "Key30"
|
||||
# 0x01CB: "Crown"
|
||||
# 0x01CB: "SOLD OUT" # replaces "Crown"
|
||||
# 0x01CC: "Ruby apple"
|
||||
# 0x01CD: "PURIFIA"
|
||||
# 0x01CE: "Tag ring"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import numbers
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from itertools import accumulate, chain, combinations
|
||||
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
|
||||
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, Range, SpecialRange, TextChoice, Toggle
|
||||
from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
|
||||
SpecialRange, TextChoice, Toggle
|
||||
from .Enemies import enemy_name_to_sprite
|
||||
from .Items import ItemType, l2ac_item_table
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
@@ -557,6 +561,25 @@ class Goal(Choice):
|
||||
default = option_boss
|
||||
|
||||
|
||||
class GoldModifier(Range):
|
||||
"""Percentage modifier for gold gained from enemies.
|
||||
|
||||
Supported values: 25 – 400
|
||||
Default value: 100 (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Gold modifier"
|
||||
range_start = 25
|
||||
range_end = 400
|
||||
default = 100
|
||||
|
||||
def __call__(self, gold: bytes) -> bytes:
|
||||
try:
|
||||
return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little")
|
||||
except OverflowError:
|
||||
return b"\xFF\xFF"
|
||||
|
||||
|
||||
class HealingFloorChance(Range):
|
||||
"""The chance of a floor having a healing tile hidden under a bush.
|
||||
|
||||
@@ -661,6 +684,105 @@ class RunSpeed(Choice):
|
||||
default = option_disabled
|
||||
|
||||
|
||||
class ShopInterval(SpecialRange):
|
||||
"""Place shops after a certain number of floors.
|
||||
|
||||
E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
|
||||
whereas if you set it to 1, then there will be a shop after every single completed floor.
|
||||
Shops will offer a random selection of wares; on deeper floors, more expensive items might appear.
|
||||
You can customize the stock that can appear in shops using the shop_inventory option.
|
||||
You can control how much gold you will be obtaining from enemies using the gold_multiplier option.
|
||||
Supported values: disabled, 1 – 10
|
||||
Default value: disabled (same as in an unmodified game)
|
||||
"""
|
||||
|
||||
display_name = "Shop interval"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 0
|
||||
special_range_cutoff = 1
|
||||
special_range_names = {
|
||||
"disabled": 0,
|
||||
}
|
||||
|
||||
|
||||
class ShopInventory(OptionDict):
|
||||
"""Determine the item types that can appear in shops.
|
||||
|
||||
The value of this option should be a mapping of item categories (or individual items) to weights (non-negative
|
||||
integers), which are used as relative probabilities when it comes to including these things in shops. (The actual
|
||||
contents of the generated shops are selected randomly and are subject to additional constraints such as more
|
||||
expensive things being allowed only on later floors.)
|
||||
Supported keys:
|
||||
non_restorative — a selection of mostly non-restorative red chest consumables
|
||||
restorative — all HP- or MP-restoring red chest consumables
|
||||
blue_chest — all blue chest items
|
||||
spell — all red chest spells
|
||||
gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option,
|
||||
meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you
|
||||
also enabled that option)
|
||||
weapon — all red chest weapons
|
||||
Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a
|
||||
higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5".
|
||||
You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing
|
||||
separate weights for each item you want to include.
|
||||
(Spells, however, cannot be weighted individually and are only available as part of the "spell" category.)
|
||||
Default value: {spell: 30, gear: 45, weapon: 82}
|
||||
"""
|
||||
|
||||
display_name = "Shop inventory"
|
||||
_special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"}
|
||||
valid_keys = _special_keys | {item for item, data in l2ac_item_table.items()
|
||||
if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST,
|
||||
ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}}
|
||||
default: Dict[str, int] = {
|
||||
"spell": 30,
|
||||
"gear": 45,
|
||||
"weapon": 82,
|
||||
}
|
||||
value: Dict[str, int]
|
||||
|
||||
def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
super().verify(world, player_name, plando_options)
|
||||
for item, weight in self.value.items():
|
||||
if not isinstance(weight, numbers.Integral) or weight < 0:
|
||||
raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, "
|
||||
f"but was \"{weight}\".")
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return sum(self.value.values())
|
||||
|
||||
@property
|
||||
def non_restorative(self) -> int:
|
||||
return self.value.get("non_restorative", 0)
|
||||
|
||||
@property
|
||||
def restorative(self) -> int:
|
||||
return self.value.get("restorative", 0)
|
||||
|
||||
@property
|
||||
def blue_chest(self) -> int:
|
||||
return self.value.get("blue_chest", 0)
|
||||
|
||||
@property
|
||||
def spell(self) -> int:
|
||||
return self.value.get("spell", 0)
|
||||
|
||||
@property
|
||||
def gear(self) -> int:
|
||||
return self.value.get("gear", 0)
|
||||
|
||||
@property
|
||||
def weapon(self) -> int:
|
||||
return self.value.get("weapon", 0)
|
||||
|
||||
@functools.cached_property
|
||||
def custom(self) -> Dict[int, int]:
|
||||
return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items()
|
||||
if item not in self._special_keys}
|
||||
|
||||
|
||||
class ShuffleCapsuleMonsters(Toggle):
|
||||
"""Shuffle the capsule monsters into the multiworld.
|
||||
|
||||
@@ -697,7 +819,7 @@ class ShufflePartyMembers(Toggle):
|
||||
|
||||
|
||||
@dataclass
|
||||
class L2ACOptions:
|
||||
class L2ACOptions(PerGameCommonOptions):
|
||||
blue_chest_chance: BlueChestChance
|
||||
blue_chest_count: BlueChestCount
|
||||
boss: Boss
|
||||
@@ -716,6 +838,7 @@ class L2ACOptions:
|
||||
final_floor: FinalFloor
|
||||
gear_variety_after_b9: GearVarietyAfterB9
|
||||
goal: Goal
|
||||
gold_modifier: GoldModifier
|
||||
healing_floor_chance: HealingFloorChance
|
||||
initial_floor: InitialFloor
|
||||
iris_floor_chance: IrisFloorChance
|
||||
@@ -723,5 +846,7 @@ class L2ACOptions:
|
||||
master_hp: MasterHp
|
||||
party_starting_level: PartyStartingLevel
|
||||
run_speed: RunSpeed
|
||||
shop_interval: ShopInterval
|
||||
shop_inventory: ShopInventory
|
||||
shuffle_capsule_monsters: ShuffleCapsuleMonsters
|
||||
shuffle_party_members: ShufflePartyMembers
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
from random import Random
|
||||
from typing import Dict, List, MutableSequence, Sequence, Set, Tuple
|
||||
from typing import Dict, Iterable, List, MutableSequence, Sequence, Set, Tuple
|
||||
|
||||
|
||||
def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]:
|
||||
@@ -19,3 +21,10 @@ def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None
|
||||
i, j = random.randrange(n), random.randrange(n)
|
||||
if x[i] in constraints[j] and x[j] in constraints[i]:
|
||||
x[i], x[j] = x[j], x[i]
|
||||
|
||||
|
||||
def weighted_sample(population: Iterable[int], weights: Iterable[float], k: int, *, random: Random) -> List[int]:
|
||||
population, keys = zip(*((item, pow(random.random(), 1 / group_weight))
|
||||
for item, group in itertools.groupby(sorted(zip(population, weights)), key=itemgetter(0))
|
||||
if (group_weight := sum(weight for _, weight in group))))
|
||||
return sorted(population, key=dict(zip(population, keys)).__getitem__)[-k:]
|
||||
|
||||
@@ -2,11 +2,11 @@ import base64
|
||||
import itertools
|
||||
import os
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
|
||||
from typing import Any, ClassVar, Dict, Iterator, List, Set, Tuple, Type
|
||||
|
||||
import settings
|
||||
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from Options import AssembleOptions
|
||||
from Options import PerGameCommonOptions
|
||||
from Utils import __version__
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
@@ -14,9 +14,9 @@ from .Client import L2ACSNIClient # noqa: F401
|
||||
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
|
||||
from .Locations import l2ac_location_name_to_id, L2ACLocation
|
||||
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
|
||||
ExpModifier, Goal, L2ACOptions
|
||||
Goal, L2ACOptions
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
|
||||
from .Utils import constrained_choices, constrained_shuffle
|
||||
from .Utils import constrained_choices, constrained_shuffle, weighted_sample
|
||||
from .basepatch import apply_basepatch
|
||||
|
||||
CHESTS_PER_SPHERE: int = 5
|
||||
@@ -54,7 +54,8 @@ class L2ACWorld(World):
|
||||
game: ClassVar[str] = "Lufia II Ancient Cave"
|
||||
web: ClassVar[WebWorld] = L2ACWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = get_type_hints(L2ACOptions)
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = L2ACOptions
|
||||
options: L2ACOptions
|
||||
settings: ClassVar[L2ACSettings]
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = l2ac_item_name_to_id
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = l2ac_location_name_to_id
|
||||
@@ -87,7 +88,7 @@ class L2ACWorld(World):
|
||||
bytearray(f"L2AC{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.o = L2ACOptions(**{opt: getattr(self.multiworld, opt)[self.player] for opt in self.option_definitions})
|
||||
self.o = self.options
|
||||
|
||||
if self.o.blue_chest_count < self.o.custom_item_pool.count:
|
||||
raise ValueError(f"Number of items in custom_item_pool ({self.o.custom_item_pool.count}) is "
|
||||
@@ -221,6 +222,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
|
||||
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
|
||||
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
|
||||
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
|
||||
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
|
||||
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
|
||||
@@ -228,6 +230,7 @@ class L2ACWorld(World):
|
||||
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
|
||||
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[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()
|
||||
@@ -356,7 +359,7 @@ class L2ACWorld(World):
|
||||
def get_enemy_stats(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if self.o.exp_modifier == ExpModifier.default:
|
||||
if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
|
||||
return rom[0x0B05C0:0x0B05C0 + 18843]
|
||||
|
||||
number_of_enemies: int = 224
|
||||
@@ -365,6 +368,7 @@ class L2ACWorld(World):
|
||||
for enemy_id in range(number_of_enemies):
|
||||
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
|
||||
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
|
||||
enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
|
||||
return enemy_stats
|
||||
|
||||
def get_goal_text_bytes(self) -> bytes:
|
||||
@@ -382,6 +386,90 @@ class L2ACWorld(World):
|
||||
goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00))
|
||||
return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes))
|
||||
|
||||
def get_shops(self) -> bytes:
|
||||
rom: bytes = get_base_rom_bytes()
|
||||
|
||||
if not self.o.shop_interval:
|
||||
return rom[0x0BEE9F:0x0BEE9F + 1948]
|
||||
|
||||
non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
|
||||
restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
|
||||
blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
|
||||
number_of_spells: int = 35
|
||||
number_of_items: int = 467
|
||||
spells_offset: int = 0x0AFA5B
|
||||
items_offset: int = 0x0B4F69
|
||||
non_restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
restorative_list: List[List[int]] = [list() for _ in range(99)]
|
||||
blue_list: List[List[int]] = [list() for _ in range(99)]
|
||||
spell_list: List[List[int]] = [list() for _ in range(99)]
|
||||
gear_list: List[List[int]] = [list() for _ in range(99)]
|
||||
weapon_list: List[List[int]] = [list() for _ in range(99)]
|
||||
custom_list: List[List[int]] = [list() for _ in range(99)]
|
||||
|
||||
for spell_id in range(number_of_spells):
|
||||
pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
|
||||
value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
|
||||
for f in range(value // 1000, 99):
|
||||
spell_list[f].append(spell_id)
|
||||
for item_id in range(number_of_items):
|
||||
pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
|
||||
buckets: List[List[List[int]]] = list()
|
||||
if item_id in non_restorative_ids:
|
||||
buckets.append(non_restorative_list)
|
||||
if item_id in restorative_ids:
|
||||
buckets.append(restorative_list)
|
||||
if item_id in blue_ids:
|
||||
buckets.append(blue_list)
|
||||
if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
|
||||
category: int = rom[items_offset + pointer + 7]
|
||||
if category >= 0x02:
|
||||
buckets.append(gear_list)
|
||||
elif category == 0x01:
|
||||
buckets.append(weapon_list)
|
||||
if item_id in self.o.shop_inventory.custom:
|
||||
buckets.append(custom_list)
|
||||
value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
|
||||
for bucket in buckets:
|
||||
for f in range(value // 1000, 99):
|
||||
bucket[f].append(item_id)
|
||||
|
||||
if not self.o.gear_variety_after_b9:
|
||||
for f in range(99):
|
||||
del gear_list[f][len(gear_list[f]) % 128:]
|
||||
|
||||
def create_shop(floor: int) -> Tuple[int, ...]:
|
||||
if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
|
||||
return create_spell_shop(floor)
|
||||
else:
|
||||
return create_item_shop(floor)
|
||||
|
||||
def create_spell_shop(floor: int) -> Tuple[int, ...]:
|
||||
spells = self.random.sample(spell_list[floor], 3)
|
||||
return 0x03, 0x20, 0x00, *spells, 0xFF
|
||||
|
||||
def create_item_shop(floor: int) -> Tuple[int, ...]:
|
||||
population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
|
||||
+ gear_list[floor] + weapon_list[floor] + custom_list[floor]
|
||||
weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
|
||||
[(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
|
||||
(self.o.shop_inventory.restorative, restorative_list[floor]),
|
||||
(self.o.shop_inventory.blue_chest, blue_list[floor]),
|
||||
(self.o.shop_inventory.gear, gear_list[floor]),
|
||||
(self.o.shop_inventory.weapon, weapon_list[floor])]),
|
||||
(self.o.shop_inventory.custom[item] for item in custom_list[floor]))
|
||||
items = weighted_sample(population, weights, 5, random=self.random)
|
||||
return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
|
||||
|
||||
shops = [create_shop(floor)
|
||||
for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
|
||||
for _ in range(self.o.shop_interval)]
|
||||
shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
|
||||
shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
|
||||
|
||||
assert len(shop_bytes) <= 1948, shop_bytes
|
||||
return shop_bytes.ljust(1948, b"\x00")
|
||||
|
||||
@staticmethod
|
||||
def get_node_connection_table() -> bytes:
|
||||
class Connect(IntFlag):
|
||||
|
||||
@@ -71,6 +71,11 @@ org $9EDD60 ; name
|
||||
org $9FA900 ; sprite
|
||||
incbin "ap_logo/ap_logo.bin"
|
||||
warnpc $9FA980
|
||||
; sold out item
|
||||
org $96F9BA ; properties
|
||||
DB $00,$00,$00,$10,$00,$00,$00,$00,$00,$00,$00,$00,$00
|
||||
org $9EDD6C ; name
|
||||
DB "SOLD OUT " ; overwrites "Crown "
|
||||
|
||||
|
||||
org $D08000 ; signature, start of expanded data area
|
||||
@@ -825,6 +830,119 @@ SpellRNG:
|
||||
|
||||
|
||||
|
||||
; shops
|
||||
pushpc
|
||||
org $83B442
|
||||
; DB=$83, x=1, m=1
|
||||
JSL Shop ; overwrites STA $7FD0BF
|
||||
pullpc
|
||||
|
||||
Shop:
|
||||
STA $7FD0BF ; (overwritten instruction)
|
||||
LDY $05AC ; load map number
|
||||
CPY.b #$F0 ; check if ancient cave
|
||||
BCC +
|
||||
LDA $05B4 ; check if going to ancient cave entrance
|
||||
BEQ +
|
||||
LDA $7FE696 ; load next to next floor number
|
||||
DEC
|
||||
CPY.b #$F1 ; check if going to final floor
|
||||
BCS ++ ; skip a decrement because next floor number is not incremented on final floor
|
||||
DEC
|
||||
++: CMP $D08015 ; check if past initial floor
|
||||
BCC +
|
||||
STA $4204 ; WRDIVL; dividend = floor number
|
||||
STZ $4205 ; WRDIVH
|
||||
TAX
|
||||
LDA $D0801A
|
||||
STA $4206 ; WRDIVB; divisor = shop_interval
|
||||
STA $211C ; M7B; second factor = shop_interval
|
||||
JSL $8082C7 ; advance RNG (while waiting for division to complete)
|
||||
LDY $4216 ; RDMPYL; skip if remainder (i.e., floor number mod shop_interval) is not 0
|
||||
BNE +
|
||||
STA $211B
|
||||
STZ $211B ; M7A; first factor = random number from 0 to 255
|
||||
TXA
|
||||
CLC
|
||||
SBC $2135 ; MPYM; calculate (floor number) - (random number from 0 to shop_interval-1) - 1
|
||||
STA $30 ; set shop id
|
||||
STZ $05A8 ; initialize variable for sold out item tracking
|
||||
STZ $05A9
|
||||
PHB
|
||||
PHP
|
||||
JML $80A33A ; open shop menu
|
||||
+: RTL
|
||||
|
||||
; shop item select
|
||||
pushpc
|
||||
org $82DF50
|
||||
; DB=$83, x=0, m=1
|
||||
JML ShopItemSelected ; overwrites JSR $8B08 : CMP.b #$01
|
||||
pullpc
|
||||
|
||||
ShopItemSelected:
|
||||
LDA $1548 ; check inventory free space
|
||||
BEQ +
|
||||
JSR LoadShopSlotAsFlag
|
||||
BIT $05A8 ; test item not already sold
|
||||
BNE +
|
||||
JML $82DF79 ; skip quantity selection and go directly to buy/equip
|
||||
+: JML $82DF80 ; abort and go back to item selection
|
||||
|
||||
; track bought shop items
|
||||
pushpc
|
||||
org $82E084
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopBuy ; overwrites LDA.b #$05 : LDX.w #$0007
|
||||
NOP
|
||||
org $82E10E
|
||||
; DB=$83, x=0, m=1
|
||||
JSL ShopEquip ; overwrites SEP #$10 : LDX $14DC
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
ShopBuy:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
LDA.b #$05 ; (overwritten instruction)
|
||||
LDX.w #$0007 ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
ShopEquip:
|
||||
JSR LoadShopSlotAsFlag
|
||||
TSB $05A8 ; mark item as sold
|
||||
SEP #$10 ; (overwritten instruction)
|
||||
LDX $14DC ; (overwritten instruction)
|
||||
RTL
|
||||
|
||||
LoadShopSlotAsFlag:
|
||||
TDC
|
||||
LDA $14EC ; load currently selected shop slot number
|
||||
ASL
|
||||
TAX
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
RTS
|
||||
|
||||
; mark bought items as sold out
|
||||
pushpc
|
||||
org $8285EA
|
||||
; DB=$83, x=0, m=0
|
||||
JSL SoldOut ; overwrites LDA [$FC],Y : AND #$01FF
|
||||
NOP
|
||||
pullpc
|
||||
|
||||
SoldOut:
|
||||
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
|
||||
BIT $05A8 ; test sold items
|
||||
BEQ +
|
||||
LDA.w #$01CB ; load sold out item id
|
||||
BRA ++
|
||||
+: LDA [$FC],Y ; (overwritten instruction)
|
||||
AND #$01FF ; (overwritten instruction)
|
||||
++: RTL
|
||||
|
||||
|
||||
|
||||
; increase variety of red chest gear after B9
|
||||
pushpc
|
||||
org $839176
|
||||
@@ -1009,6 +1127,53 @@ pullpc
|
||||
|
||||
|
||||
|
||||
; door stairs fix
|
||||
pushpc
|
||||
org $839453
|
||||
; DB=$7F, x=0, m=1
|
||||
JSL DoorStairsFix ; overwrites JSR $9B18 : JSR $9D11
|
||||
NOP #2
|
||||
pullpc
|
||||
|
||||
DoorStairsFix:
|
||||
CLC
|
||||
LDY.w #$0000
|
||||
--: LDX.w #$00FF ; loop through floor layout starting from the bottom right
|
||||
-: LDA $EA00,X ; read node contents
|
||||
BEQ + ; always skip empty nodes
|
||||
BCC ++ ; 1st pass: skip all blocked nodes (would cause door stairs or rare stairs)
|
||||
LDA $E9F0,X ; 2nd pass: skip only if the one above is also blocked (would cause door stairs)
|
||||
++: BMI +
|
||||
INY ; count usable nodes
|
||||
+: DEX
|
||||
BPL -
|
||||
TYA
|
||||
BNE ++ ; all nodes blocked?
|
||||
SEC ; set up 2nd, less restrictive pass
|
||||
BRA --
|
||||
++: JSL $8082C7 ; advance RNG
|
||||
STA $00211B
|
||||
TDC
|
||||
STA $00211B ; M7A; first factor = random number from 0 to 255
|
||||
TYA
|
||||
STA $00211C ; M7B; second factor = number of possible stair positions
|
||||
LDA $002135 ; MPYM; calculate random number from 0 to number of possible stair positions - 1
|
||||
TAY
|
||||
LDX.w #$00FF ; loop through floor layout starting from the bottom right
|
||||
-: LDA $EA00,X ; read node contents
|
||||
BEQ + ; always skip empty nodes
|
||||
BCC ++ ; if 1st pass was sufficient: skip all blocked nodes (prevent door stairs and rare stairs)
|
||||
LDA $E9F0,X ; if 2nd pass was needed: skip only if the one above is also blocked (prevent door stairs)
|
||||
++: BMI +
|
||||
DEY ; count down to locate the (Y+1)th usable node
|
||||
BMI ++
|
||||
+: DEX
|
||||
BPL -
|
||||
++: TXA ; return selected stair node coordinate
|
||||
RTL
|
||||
|
||||
|
||||
|
||||
; equipment text fix
|
||||
pushpc
|
||||
org $81F2E3
|
||||
@@ -1054,6 +1219,7 @@ pullpc
|
||||
; $F02017 1 iris treasures required
|
||||
; $F02018 1 party members available
|
||||
; $F02019 1 capsule monsters available
|
||||
; $F0201A 1 shop interval
|
||||
; $F02030 1 selected goal
|
||||
; $F02031 1 goal completion: boss
|
||||
; $F02032 1 goal completion: iris_treasure_hunt
|
||||
|
||||
Binary file not shown.
@@ -49,8 +49,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
||||
- Customize the multiworld item pool. (By default, your pool is filled with random blue chest items, but you can place
|
||||
any cave item you want instead)
|
||||
- Customize start inventory, i.e., begin every run with certain items or spells of your choice
|
||||
- Adjust how much EXP is gained from enemies
|
||||
- Adjust how much EXP and gold is gained from enemies
|
||||
- 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
|
||||
@@ -75,7 +76,7 @@ Your Party Leader will hold up the item they received when not in a fight or in
|
||||
|
||||
###### Bug fixes:
|
||||
|
||||
- Vanilla game bugs that could result in softlocks or save file corruption have been fixed
|
||||
- Vanilla game bugs that could result in anomalous floors, softlocks, or save file corruption have been fixed
|
||||
- (optional) Bugfix for the algorithm that determines the item pool for red chest gear. Enabling this allows the cave to
|
||||
generate shields, headgear, rings, and jewels in red chests even after floor B9
|
||||
- (optional) Bugfix for the outlandish cravings of capsule monsters in the US version. Enabling this makes feeding work
|
||||
|
||||
@@ -44,7 +44,7 @@ your personal settings and export a config file from them.
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
[YAML Validator](/check) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -82,9 +82,7 @@ class MessengerWorld(World):
|
||||
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
|
||||
if region.name in REGION_CONNECTIONS:
|
||||
region.add_exits(REGION_CONNECTIONS[region.name])
|
||||
self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS]
|
||||
|
||||
def create_items(self) -> None:
|
||||
# create items that are always in the item pool
|
||||
@@ -138,6 +136,8 @@ class MessengerWorld(World):
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def set_rules(self) -> None:
|
||||
for reg_name, connections in REGION_CONNECTIONS.items():
|
||||
self.multiworld.get_region(reg_name, self.player).add_exits(connections)
|
||||
logic = self.options.logic_level
|
||||
if logic == Logic.option_normal:
|
||||
MessengerRules(self).set_messenger_rules()
|
||||
|
||||
@@ -32,7 +32,6 @@ class MessengerRegion(Region):
|
||||
loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
|
||||
for loc in locations}
|
||||
self.add_locations(loc_dict, MessengerLocation)
|
||||
world.multiworld.regions.append(self)
|
||||
|
||||
|
||||
class MessengerLocation(Location):
|
||||
|
||||
@@ -6,7 +6,7 @@ class SongData(NamedTuple):
|
||||
"""Special data container to contain the metadata of each song to make filtering work."""
|
||||
|
||||
code: Optional[int]
|
||||
song_is_free: bool
|
||||
album: str
|
||||
streamer_mode: bool
|
||||
easy: Optional[int]
|
||||
hard: Optional[int]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from .Items import SongData, AlbumData
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Set, Optional
|
||||
from collections import ChainMap
|
||||
|
||||
|
||||
@@ -15,13 +15,21 @@ class MuseDashCollections:
|
||||
MUSIC_SHEET_NAME: str = "Music Sheet"
|
||||
MUSIC_SHEET_CODE: int = STARTING_CODE
|
||||
|
||||
FREE_ALBUMS = [
|
||||
FREE_ALBUMS: List[str] = [
|
||||
"Default Music",
|
||||
"Budget Is Burning: Nano Core",
|
||||
"Budget Is Burning Vol.1",
|
||||
]
|
||||
|
||||
DIFF_OVERRIDES = [
|
||||
MUSE_PLUS_DLC: str = "Muse Plus"
|
||||
DLC: List[str] = [
|
||||
# MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings.
|
||||
# "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
|
||||
"Miku in Museland", # Paid DLC not included in Muse Plus
|
||||
"MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024.
|
||||
]
|
||||
|
||||
DIFF_OVERRIDES: List[str] = [
|
||||
"MuseDash ka nanika hi",
|
||||
"Rush-Hour",
|
||||
"Find this Month's Featured Playlist",
|
||||
@@ -48,8 +56,8 @@ class MuseDashCollections:
|
||||
"Error SFX Trap": STARTING_CODE + 9,
|
||||
}
|
||||
|
||||
item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items)
|
||||
location_names_to_id = ChainMap(song_locations, album_locations)
|
||||
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
|
||||
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE
|
||||
@@ -70,7 +78,6 @@ class MuseDashCollections:
|
||||
# Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff'
|
||||
song_name = sections[0]
|
||||
# [1] is used in the client copy to make sure item id's match.
|
||||
song_is_free = album in self.FREE_ALBUMS
|
||||
steamer_mode = sections[3] == "True"
|
||||
|
||||
if song_name in self.DIFF_OVERRIDES:
|
||||
@@ -84,7 +91,7 @@ class MuseDashCollections:
|
||||
diff_of_hard = self.parse_song_difficulty(sections[5])
|
||||
diff_of_master = self.parse_song_difficulty(sections[6])
|
||||
|
||||
self.song_items[song_name] = SongData(item_id_index, song_is_free, steamer_mode,
|
||||
self.song_items[song_name] = SongData(item_id_index, album, steamer_mode,
|
||||
diff_of_easy, diff_of_hard, diff_of_master)
|
||||
item_id_index += 1
|
||||
|
||||
@@ -102,13 +109,13 @@ class MuseDashCollections:
|
||||
self.song_locations[f"{name}-1"] = location_id_index + 1
|
||||
location_id_index += 2
|
||||
|
||||
def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
|
||||
def get_songs_with_settings(self, dlc_songs: Set[str], streamer_mode_active: bool,
|
||||
diff_lower: int, diff_higher: int) -> List[str]:
|
||||
"""Gets a list of all songs that match the filter settings. Difficulty thresholds are inclusive."""
|
||||
filtered_list = []
|
||||
|
||||
for songKey, songData in self.song_items.items():
|
||||
if not dlc_songs and not songData.song_is_free:
|
||||
if not self.song_matches_dlc_filter(songData, dlc_songs):
|
||||
continue
|
||||
|
||||
if streamer_mode_active and not songData.streamer_mode:
|
||||
@@ -128,6 +135,19 @@ class MuseDashCollections:
|
||||
|
||||
return filtered_list
|
||||
|
||||
def song_matches_dlc_filter(self, song: SongData, dlc_songs: Set[str]) -> bool:
|
||||
if song.album in self.FREE_ALBUMS:
|
||||
return True
|
||||
|
||||
if song.album in dlc_songs:
|
||||
return True
|
||||
|
||||
# Muse Plus provides access to any DLC not included as a seperate pack
|
||||
if song.album not in self.DLC and self.MUSE_PLUS_DLC in dlc_songs:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def parse_song_difficulty(self, difficulty: str) -> Optional[int]:
|
||||
"""Attempts to parse the song difficulty."""
|
||||
if len(difficulty) <= 0 or difficulty == "?" or difficulty == "¿":
|
||||
|
||||
@@ -51,42 +51,42 @@ Mujinku-Vacuum|0-28|Default Music|False|5|7|11|
|
||||
MilK|0-36|Default Music|False|5|7|9|
|
||||
umpopoff|0-41|Default Music|False|0|?|0|
|
||||
Mopemope|0-45|Default Music|False|4|7|9|11
|
||||
The Happycore Idol|43-0|Just as Planned Plus|True|2|5|7|
|
||||
Amatsumikaboshi|43-1|Just as Planned Plus|True|4|6|8|10
|
||||
ARIGA THESIS|43-2|Just as Planned Plus|True|3|6|10|
|
||||
Night of Nights|43-3|Just as Planned Plus|False|4|7|10|
|
||||
#Psychedelic_Meguro_River|43-4|Just as Planned Plus|False|3|6|8|
|
||||
can you feel it|43-5|Just as Planned Plus|False|4|6|8|9
|
||||
Midnight O'clock|43-6|Just as Planned Plus|True|3|6|8|
|
||||
Rin|43-7|Just as Planned Plus|True|5|7|10|
|
||||
Smile-mileS|43-8|Just as Planned Plus|False|6|8|10|
|
||||
Believing and Being|43-9|Just as Planned Plus|True|4|6|9|
|
||||
Catalyst|43-10|Just as Planned Plus|False|5|7|9|
|
||||
don't!stop!eroero!|43-11|Just as Planned Plus|True|5|7|9|
|
||||
pa pi pu pi pu pi pa|43-12|Just as Planned Plus|False|6|8|10|
|
||||
Sand Maze|43-13|Just as Planned Plus|True|6|8|10|11
|
||||
Diffraction|43-14|Just as Planned Plus|True|5|8|10|
|
||||
AKUMU|43-15|Just as Planned Plus|False|4|6|8|
|
||||
Queen Aluett|43-16|Just as Planned Plus|True|7|9|11|
|
||||
DROPS|43-17|Just as Planned Plus|False|2|5|8|
|
||||
Frightfully-insane Flan-chan's frightful song|43-18|Just as Planned Plus|False|5|7|10|
|
||||
snooze|43-19|Just as Planned Plus|False|5|7|10|
|
||||
Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|Just as Planned Plus|True|5|7|9|
|
||||
Inu no outa|43-21|Just as Planned Plus|True|3|5|7|
|
||||
Prism Fountain|43-22|Just as Planned Plus|True|7|9|11|
|
||||
Gospel|43-23|Just as Planned Plus|False|4|6|9|
|
||||
The Happycore Idol|43-0|MD Plus Project|True|2|5|7|
|
||||
Amatsumikaboshi|43-1|MD Plus Project|True|4|6|8|10
|
||||
ARIGA THESIS|43-2|MD Plus Project|True|3|6|10|
|
||||
Night of Nights|43-3|MD Plus Project|False|4|7|10|
|
||||
#Psychedelic_Meguro_River|43-4|MD Plus Project|False|3|6|8|
|
||||
can you feel it|43-5|MD Plus Project|False|4|6|8|9
|
||||
Midnight O'clock|43-6|MD Plus Project|True|3|6|8|
|
||||
Rin|43-7|MD Plus Project|True|5|7|10|
|
||||
Smile-mileS|43-8|MD Plus Project|False|6|8|10|
|
||||
Believing and Being|43-9|MD Plus Project|True|4|6|9|
|
||||
Catalyst|43-10|MD Plus Project|False|5|7|9|
|
||||
don't!stop!eroero!|43-11|MD Plus Project|True|5|7|9|
|
||||
pa pi pu pi pu pi pa|43-12|MD Plus Project|False|6|8|10|
|
||||
Sand Maze|43-13|MD Plus Project|True|6|8|10|11
|
||||
Diffraction|43-14|MD Plus Project|True|5|8|10|
|
||||
AKUMU|43-15|MD Plus Project|False|4|6|8|
|
||||
Queen Aluett|43-16|MD Plus Project|True|7|9|11|
|
||||
DROPS|43-17|MD Plus Project|False|2|5|8|
|
||||
Frightfully-insane Flan-chan's frightful song|43-18|MD Plus Project|False|5|7|10|
|
||||
snooze|43-19|MD Plus Project|False|5|7|10|
|
||||
Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|MD Plus Project|True|5|7|9|
|
||||
Inu no outa|43-21|MD Plus Project|True|3|5|7|
|
||||
Prism Fountain|43-22|MD Plus Project|True|7|9|11|
|
||||
Gospel|43-23|MD Plus Project|False|4|6|9|
|
||||
East Ai Li Lovely|62-0|Happy Otaku Pack Vol.17|False|2|4|7|
|
||||
Mori Umi no Fune|62-1|Happy Otaku Pack Vol.17|True|5|7|9|
|
||||
Ooi|62-2|Happy Otaku Pack Vol.17|True|5|7|10|
|
||||
Numatta!!|62-3|Happy Otaku Pack Vol.17|True|5|7|9|
|
||||
SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|
|
||||
SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|10
|
||||
Fantasia Sonata Colorful feat. V!C|62-5|Happy Otaku Pack Vol.17|True|6|8|11|
|
||||
MuseDash ka nanika hi|61-0|Ola Dash|True|?|?|¿|
|
||||
Aleph-0|61-1|Ola Dash|True|7|9|11|
|
||||
Buttoba Supernova|61-2|Ola Dash|False|5|7|10|11
|
||||
Rush-Hour|61-3|Ola Dash|False|IG|Jh|a2|Eh
|
||||
3rd Avenue|61-4|Ola Dash|False|3|5|〇|
|
||||
WORLDINVADER|61-5|Ola Dash|True|5|8|10|
|
||||
WORLDINVADER|61-5|Ola Dash|True|5|8|10|11
|
||||
N3V3R G3T OV3R|60-0|maimai DX Limited-time Suite|True|4|7|10|
|
||||
Oshama Scramble!|60-1|maimai DX Limited-time Suite|True|5|7|10|
|
||||
Valsqotch|60-2|maimai DX Limited-time Suite|True|5|9|11|
|
||||
@@ -450,13 +450,13 @@ Love Patrol|63-2|MUSE RADIO FM104|True|3|5|7|
|
||||
Mahorova|63-3|MUSE RADIO FM104|True|3|5|8|
|
||||
Yoru no machi|63-4|MUSE RADIO FM104|True|1|4|7|
|
||||
INTERNET YAMERO|63-5|MUSE RADIO FM104|True|6|8|10|
|
||||
Abracadabra|43-24|Just as Planned Plus|False|6|8|10|
|
||||
Squalldecimator feat. EZ-Ven|43-25|Just as Planned Plus|True|5|7|9|
|
||||
Amateras Rhythm|43-26|Just as Planned Plus|True|6|8|11|
|
||||
Record one's Dream|43-27|Just as Planned Plus|False|4|7|10|
|
||||
Lunatic|43-28|Just as Planned Plus|True|5|8|10|
|
||||
Jiumeng|43-29|Just as Planned Plus|True|3|6|8|
|
||||
The Day We Become Family|43-30|Just as Planned Plus|True|3|5|8|
|
||||
Abracadabra|43-24|MD Plus Project|False|6|8|10|
|
||||
Squalldecimator feat. EZ-Ven|43-25|MD Plus Project|True|5|7|9|
|
||||
Amateras Rhythm|43-26|MD Plus Project|True|6|8|11|
|
||||
Record one's Dream|43-27|MD Plus Project|False|4|7|10|
|
||||
Lunatic|43-28|MD Plus Project|True|5|8|10|
|
||||
Jiumeng|43-29|MD Plus Project|True|3|6|8|
|
||||
The Day We Become Family|43-30|MD Plus Project|True|3|5|8|
|
||||
Sutori ma FIRE!?!?|64-0|COSMIC RADIO PEROLIST|True|3|5|8|
|
||||
Tanuki Step|64-1|COSMIC RADIO PEROLIST|True|5|7|10|11
|
||||
Space Stationery|64-2|COSMIC RADIO PEROLIST|True|5|7|10|
|
||||
@@ -465,7 +465,27 @@ Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11
|
||||
Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8|
|
||||
Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10|
|
||||
mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11
|
||||
BrainDance|65-0|Neon Abyss|True|3|6|9|
|
||||
My Focus!|65-1|Neon Abyss|True|5|7|10|
|
||||
ABABABA BURST|65-2|Neon Abyss|True|5|7|9|
|
||||
ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10|
|
||||
BrainDance|65-0|NeonAbyss|True|3|6|9|
|
||||
My Focus!|65-1|NeonAbyss|True|5|7|10|
|
||||
ABABABA BURST|65-2|NeonAbyss|True|5|7|9|
|
||||
ULTRA HIGHER|65-3|NeonAbyss|True|4|7|10|
|
||||
Silver Bullet|43-31|MD Plus Project|True|5|7|10|
|
||||
Random|43-32|MD Plus Project|True|4|7|9|
|
||||
OTOGE-BOSS-KYOKU-CHAN|43-33|MD Plus Project|False|6|8|10|11
|
||||
Crow Rabbit|43-34|MD Plus Project|True|7|9|11|
|
||||
SyZyGy|43-35|MD Plus Project|True|6|8|10|11
|
||||
Mermaid Radio|43-36|MD Plus Project|True|3|5|7|
|
||||
Helixir|43-37|MD Plus Project|False|6|8|10|
|
||||
Highway Cruisin'|43-38|MD Plus Project|False|3|5|8|
|
||||
JACK PT BOSS|43-39|MD Plus Project|False|6|8|10|
|
||||
Time Capsule|43-40|MD Plus Project|False|7|9|11|
|
||||
39 Music!|66-0|Miku in Museland|False|3|5|8|
|
||||
Hand in Hand|66-1|Miku in Museland|False|1|3|6|
|
||||
Cynical Night Plan|66-2|Miku in Museland|False|4|6|8|
|
||||
God-ish|66-3|Miku in Museland|False|4|7|10|
|
||||
Darling Dance|66-4|Miku in Museland|False|4|7|9|
|
||||
Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|
|
||||
The Vampire|66-6|Miku in Museland|False|4|6|9|
|
||||
Future Eve|66-7|Miku in Museland|False|4|8|11|
|
||||
Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10|
|
||||
Shun-ran|66-9|Miku in Museland|False|4|7|9|
|
||||
@@ -1,10 +1,19 @@
|
||||
from typing import Dict
|
||||
from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet
|
||||
from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .MuseDashCollection import MuseDashCollections
|
||||
|
||||
class AllowJustAsPlannedDLCSongs(Toggle):
|
||||
"""Whether [Just as Planned]/[Muse Plus] DLC Songs, and all the DLCs along with it, will be included in the randomizer."""
|
||||
display_name = "Allow [Just as Planned]/[Muse Plus] DLC Songs"
|
||||
"""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"
|
||||
default = {}
|
||||
valid_keys = [dlc for dlc in MuseDashCollections.DLC]
|
||||
|
||||
|
||||
class StreamerModeEnabled(Toggle):
|
||||
@@ -159,21 +168,22 @@ class ExcludeSongs(ItemSet):
|
||||
display_name = "Exclude Songs"
|
||||
|
||||
|
||||
musedash_options: Dict[str, type(Option)] = {
|
||||
"allow_just_as_planned_dlc_songs": AllowJustAsPlannedDLCSongs,
|
||||
"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,
|
||||
"grade_needed": GradeNeeded,
|
||||
"music_sheet_count_percentage": MusicSheetCountPercentage,
|
||||
"music_sheet_win_count_percentage": MusicSheetWinCountPercentage,
|
||||
"available_trap_types": TrapTypes,
|
||||
"trap_count_percentage": TrapCountPercentage,
|
||||
"death_link": DeathLink,
|
||||
"include_songs": IncludeSongs,
|
||||
"exclude_songs": ExcludeSongs
|
||||
}
|
||||
@dataclass
|
||||
class MuseDashOptions(PerGameCommonOptions):
|
||||
allow_just_as_planned_dlc_songs: AllowJustAsPlannedDLCSongs
|
||||
dlc_packs: DLCMusicPacks
|
||||
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
|
||||
grade_needed: GradeNeeded
|
||||
music_sheet_count_percentage: MusicSheetCountPercentage
|
||||
music_sheet_win_count_percentage: MusicSheetWinCountPercentage
|
||||
available_trap_types: TrapTypes
|
||||
trap_count_percentage: TrapCountPercentage
|
||||
death_link: DeathLink
|
||||
include_songs: IncludeSongs
|
||||
exclude_songs: ExcludeSongs
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.generic.Rules import set_rule
|
||||
from BaseClasses import Region, Item, ItemClassification, Entrance, Tutorial
|
||||
from typing import List
|
||||
from typing import List, ClassVar, Type
|
||||
from math import floor
|
||||
from Options import PerGameCommonOptions
|
||||
|
||||
from .Options import musedash_options
|
||||
from .Options import MuseDashOptions
|
||||
from .Items import MuseDashSongItem, MuseDashFixedItem
|
||||
from .Locations import MuseDashLocation
|
||||
from .MuseDashCollection import MuseDashCollections
|
||||
@@ -23,7 +23,16 @@ class MuseDashWebWorld(WebWorld):
|
||||
["DeamonHunter"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
setup_es = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Español",
|
||||
"setup_es.md",
|
||||
"setup/es",
|
||||
["Shiny"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_es]
|
||||
|
||||
|
||||
class MuseDashWorld(World):
|
||||
@@ -38,9 +47,9 @@ class MuseDashWorld(World):
|
||||
|
||||
# World Options
|
||||
game = "Muse Dash"
|
||||
option_definitions = musedash_options
|
||||
options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions
|
||||
topology_present = False
|
||||
data_version = 9
|
||||
data_version = 10
|
||||
web = MuseDashWebWorld()
|
||||
|
||||
# Necessary Data
|
||||
@@ -57,14 +66,17 @@ class MuseDashWorld(World):
|
||||
location_count: int
|
||||
|
||||
def generate_early(self):
|
||||
dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
|
||||
streamer_mode = self.multiworld.streamer_mode_enabled[self.player]
|
||||
dlc_songs = {key for key in self.options.dlc_packs.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
|
||||
(lower_diff_threshold, higher_diff_threshold) = self.get_difficulty_range()
|
||||
|
||||
# The minimum amount of songs to make an ok rando would be Starting Songs + 10 interim songs + Goal song.
|
||||
# - Interim songs being equal to max starting song count.
|
||||
# Note: The worst settings still allow 25 songs (Streamer Mode + No DLC).
|
||||
starter_song_count = self.multiworld.starting_song_count[self.player].value
|
||||
starter_song_count = self.options.starting_song_count.value
|
||||
|
||||
while True:
|
||||
# In most cases this should only need to run once
|
||||
@@ -95,9 +107,9 @@ class MuseDashWorld(World):
|
||||
def handle_plando(self, available_song_keys: List[str]) -> List[str]:
|
||||
song_items = self.md_collection.song_items
|
||||
|
||||
start_items = self.multiworld.start_inventory[self.player].value.keys()
|
||||
include_songs = self.multiworld.include_songs[self.player].value
|
||||
exclude_songs = self.multiworld.exclude_songs[self.player].value
|
||||
start_items = self.options.start_inventory.value.keys()
|
||||
include_songs = self.options.include_songs.value
|
||||
exclude_songs = self.options.exclude_songs.value
|
||||
|
||||
self.starting_songs = [s for s in start_items if s in song_items]
|
||||
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
|
||||
@@ -106,8 +118,8 @@ class MuseDashWorld(World):
|
||||
and s not in include_songs and s not in exclude_songs]
|
||||
|
||||
def create_song_pool(self, available_song_keys: List[str]):
|
||||
starting_song_count = self.multiworld.starting_song_count[self.player].value
|
||||
additional_song_count = self.multiworld.additional_song_count[self.player].value
|
||||
starting_song_count = self.options.starting_song_count.value
|
||||
additional_song_count = self.options.additional_song_count.value
|
||||
|
||||
self.random.shuffle(available_song_keys)
|
||||
|
||||
@@ -141,7 +153,7 @@ class MuseDashWorld(World):
|
||||
|
||||
# Then attempt to fufill any remaining songs for interim songs
|
||||
if len(self.included_songs) < additional_song_count:
|
||||
for _ in range(len(self.included_songs), self.multiworld.additional_song_count[self.player]):
|
||||
for _ in range(len(self.included_songs), self.options.additional_song_count):
|
||||
if len(available_song_keys) <= 0:
|
||||
break
|
||||
self.included_songs.append(available_song_keys.pop())
|
||||
@@ -249,40 +261,40 @@ class MuseDashWorld(World):
|
||||
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
|
||||
|
||||
def get_available_traps(self) -> List[str]:
|
||||
dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
|
||||
sfx_traps_available = self.options.allow_just_as_planned_dlc_songs.value
|
||||
|
||||
trap_list = []
|
||||
if self.multiworld.available_trap_types[self.player].value & 1 != 0:
|
||||
if self.options.available_trap_types.value & 1 != 0:
|
||||
trap_list += self.md_collection.vfx_trap_items.keys()
|
||||
|
||||
# SFX options are only available under Just as Planned DLC.
|
||||
if dlc_songs and self.multiworld.available_trap_types[self.player].value & 2 != 0:
|
||||
if sfx_traps_available and self.options.available_trap_types.value & 2 != 0:
|
||||
trap_list += self.md_collection.sfx_trap_items.keys()
|
||||
|
||||
return trap_list
|
||||
|
||||
def get_additional_item_percentage(self) -> int:
|
||||
trap_count = self.multiworld.trap_count_percentage[self.player].value
|
||||
song_count = self.multiworld.music_sheet_count_percentage[self.player].value
|
||||
return max(trap_count + song_count, self.multiworld.additional_item_percentage[self.player].value)
|
||||
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.multiworld.trap_count_percentage[self.player].value / 100.0
|
||||
multiplier = self.options.trap_count_percentage.value / 100.0
|
||||
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
||||
return max(0, floor(trap_count * multiplier))
|
||||
|
||||
def get_music_sheet_count(self) -> int:
|
||||
multiplier = self.multiworld.music_sheet_count_percentage[self.player].value / 100.0
|
||||
multiplier = self.options.music_sheet_count_percentage.value / 100.0
|
||||
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
|
||||
return max(1, floor(song_count * multiplier))
|
||||
|
||||
def get_music_sheet_win_count(self) -> int:
|
||||
multiplier = self.multiworld.music_sheet_win_count_percentage[self.player].value / 100.0
|
||||
multiplier = self.options.music_sheet_win_count_percentage.value / 100.0
|
||||
sheet_count = self.get_music_sheet_count()
|
||||
return max(1, floor(sheet_count * multiplier))
|
||||
|
||||
def get_difficulty_range(self) -> List[int]:
|
||||
difficulty_mode = self.multiworld.song_difficulty_mode[self.player]
|
||||
difficulty_mode = self.options.song_difficulty_mode
|
||||
|
||||
# Valid difficulties are between 1 and 11. But make it 0 to 12 for safety
|
||||
difficulty_bounds = [0, 12]
|
||||
@@ -300,8 +312,8 @@ class MuseDashWorld(World):
|
||||
elif difficulty_mode == 5:
|
||||
difficulty_bounds[0] = 10
|
||||
elif difficulty_mode == 6:
|
||||
minimum_difficulty = self.multiworld.song_difficulty_min[self.player].value
|
||||
maximum_difficulty = self.multiworld.song_difficulty_max[self.player].value
|
||||
minimum_difficulty = self.options.song_difficulty_min.value
|
||||
maximum_difficulty = self.options.song_difficulty_max.value
|
||||
|
||||
difficulty_bounds[0] = min(minimum_difficulty, maximum_difficulty)
|
||||
difficulty_bounds[1] = max(minimum_difficulty, maximum_difficulty)
|
||||
@@ -311,7 +323,7 @@ class MuseDashWorld(World):
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"victoryLocation": self.victory_song_name,
|
||||
"deathLink": self.multiworld.death_link[self.player].value,
|
||||
"deathLink": self.options.death_link.value,
|
||||
"musicSheetWinCount": self.get_music_sheet_win_count(),
|
||||
"gradeNeeded": self.multiworld.grade_needed[self.player].value
|
||||
"gradeNeeded": self.options.grade_needed.value
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
- Windows 8 or Newer.
|
||||
- Muse Dash: [Available on Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
|
||||
- \[Optional\] [Just as Planned] DLC: [Also Available on Steam](https://store.steampowered.com/app/1055810/Muse_Dash__Just_as_planned/)
|
||||
- \[Optional\] [Muse Plus] DLC: [Also Available on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
|
||||
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
|
||||
- .Net Framework 4.8 may be needed for the installer: [Download](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)
|
||||
- .Net 6.0 (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0#runtime-6.0.15)
|
||||
- .NET Desktop Runtime 6.0.XX (If not already installed): [Download](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
|
||||
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
|
||||
|
||||
## Installing the Archipelago mod to Muse Dash
|
||||
|
||||
48
worlds/musedash/docs/setup_es.md
Normal file
48
worlds/musedash/docs/setup_es.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Guía de instalación para Muse Dash: Archipelago
|
||||
|
||||
## Enlaces rápidos
|
||||
- [Página Principal](../../../../games/Muse%20Dash/info/en)
|
||||
- [Página de Configuraciones](../../../../games/Muse%20Dash/player-settings)
|
||||
|
||||
## Software Requerido
|
||||
|
||||
- Windows 8 o más reciente.
|
||||
- Muse Dash: [Disponible en Steam](https://store.steampowered.com/app/774171/Muse_Dash/)
|
||||
- \[Opcional\] [Muse Plus] DLC: [tambien disponible on Steam](https://store.steampowered.com/app/2593750/Muse_Dash__Muse_Plus/)
|
||||
- Melon Loader: [GitHub](https://github.com/LavaGang/MelonLoader/releases/latest)
|
||||
- .Net Framework 4.8 podría ser necesario para el instalador: [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet-framework/net48)
|
||||
- Entorno de ejecución de escritorio de .NET 6.0.XX (si aún no está instalado): [Descarga](https://dotnet.microsoft.com/es-es/download/dotnet/6.0)
|
||||
- Muse Dash Archipelago Mod: [GitHub](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest)
|
||||
|
||||
## Instalar el mod de Archipelago en Muse Dash
|
||||
|
||||
1. Descarga [MelonLoader.Installer.exe](https://github.com/LavaGang/MelonLoader/releases/latest) y ejecutalo.
|
||||
2. Elije la pestaña "automated", haz clic en el botón "select" y busca tu `MuseDash.exe`. Luego haz clic en "install".
|
||||
- Puedes encontrar la carpeta en Steam buscando el juego en tu biblioteca, haciendo clic derecho sobre el y elegir *Administrar→Ver archivos locales*.
|
||||
- Si haces clic en la barra superior que te indica la carpeta en la que estas, te dará la dirección de ésta para que puedas copiarla. Al pegar esa dirección en la ventana que **MelonLoader** abre, irá automaticamente a esa carpeta.
|
||||
3. Ejecuta el juego una vez, y espera hasta que aparezca la pantalla de inicio de Muse Dash antes de cerrarlo.
|
||||
4. Descarga la última version de [Muse Dash Archipelago Mod](https://github.com/DeamonHunter/ArchipelagoMuseDash/releases/latest) y extraelo en la nueva carpeta creada llamada `/Mods/`, localizada en la carpeta de instalación de Muse Dash.
|
||||
- Todos los archivos deben ir directamente en la carpeta `/Mods/`, y NO en una subcarpeta dentro de la carpeta `/Mods/`
|
||||
|
||||
Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago.
|
||||
|
||||
## Generar un juego MultiWorld
|
||||
1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-settings) y configura las opciones del juego a tu gusto.
|
||||
2. Genera tu archivo YAML y úsalo para generar un juego nuevo en el radomizer
|
||||
- (Instrucciones sobre como generar un juego en Archipelago disponibles en la [guía web de Archipelago en Inglés](/tutorial/Archipelago/setup/en))
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
1. Ejecuta Muse Dash y pasa por la pantalla de introducción. Haz clic en el botón de la esquina inferior derecha.
|
||||
2. Ingresa los detalles de la sesión de archipelago, como la dirección del servidor con el puerto (por ejemplo, archipelago.gg:38381), nombre de usuario y contraseña.
|
||||
3. Si todo se ingresó correctamente, el pop-up debería desaparecer y se mostrará el menú principal habitual. Al ingresar a la selección de canciones, deberías ver una cantidad limitada de canciones.
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### No Support Module Loaded
|
||||
|
||||
Este error ocurre cuando Melon Loader no puede encontrar los archivos necesarios para ejecutar mods. Generalmente, hay dos razones principales de este error: una falla al generar los archivos cuando el juego se ejecutó por primera vez con Melon Loader, o un antivirus que elimina los archivos después de la generación.
|
||||
|
||||
Para solucionar este problema, primero debes eliminar Melon Loader de Muse Dash. Puedes hacer esto eliminando la carpeta Melon Loader dentro de la carpeta de Muse Dash. Luego, seguir los pasos de instalación nuevamente.
|
||||
|
||||
Si continúas teniendo problemas y estás utilizando un antivirus, es posible que tengas que desactivarlo temporalmente cuando se ejecute Muse Dash por primera vez, o excluir la carpeta Muse Dash de ser escaneada.
|
||||
@@ -36,14 +36,27 @@ class CollectionsTest(unittest.TestCase):
|
||||
|
||||
def test_free_dlc_included_in_base_songs(self) -> None:
|
||||
collection = MuseDashCollections()
|
||||
songs = collection.get_songs_with_settings(False, False, 0, 11)
|
||||
songs = collection.get_songs_with_settings(set(), False, 0, 12)
|
||||
|
||||
self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs")
|
||||
self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs")
|
||||
|
||||
def test_dlcs(self) -> None:
|
||||
collection = MuseDashCollections()
|
||||
free_song_count = len(collection.get_songs_with_settings(set(), False, 0, 12))
|
||||
known_mp_song = "The Happycore Idol"
|
||||
|
||||
for dlc in collection.DLC:
|
||||
songs_with_dlc = collection.get_songs_with_settings({dlc}, False, 0, 12)
|
||||
self.assertGreater(len(songs_with_dlc), free_song_count, f"DLC {dlc} did not include extra songs.")
|
||||
if dlc == collection.MUSE_PLUS_DLC:
|
||||
self.assertIn(known_mp_song, songs_with_dlc, f"Muse Plus missing muse plus song.")
|
||||
else:
|
||||
self.assertNotIn(known_mp_song, songs_with_dlc, f"DLC {dlc} includes Muse Plus songs.")
|
||||
|
||||
def test_remove_songs_are_not_generated(self) -> None:
|
||||
collection = MuseDashCollections()
|
||||
songs = collection.get_songs_with_settings(True, False, 0, 11)
|
||||
songs = collection.get_songs_with_settings({x for x in collection.DLC}, False, 0, 12)
|
||||
|
||||
for song_name in self.REMOVED_SONGS:
|
||||
self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.")
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import MuseDashTestBase
|
||||
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]
|
||||
@@ -12,7 +13,7 @@ class DifficultyRanges(MuseDashTestBase):
|
||||
self.assertEqual(inputRange[0], lower)
|
||||
self.assertEqual(inputRange[1], upper)
|
||||
|
||||
songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1])
|
||||
songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, inputRange[0], inputRange[1])
|
||||
for songKey in songs:
|
||||
song = muse_dash_world.md_collection.song_items[songKey]
|
||||
if (song.easy is not None and inputRange[0] <= song.easy <= inputRange[1]):
|
||||
|
||||
@@ -350,7 +350,7 @@ def generate_itempool(ootworld):
|
||||
ootworld.itempool = [ootworld.create_item(item) for item in pool]
|
||||
for (location_name, item) in placed_items.items():
|
||||
location = world.get_location(location_name, player)
|
||||
location.place_locked_item(ootworld.create_item(item))
|
||||
location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True))
|
||||
|
||||
|
||||
def get_pool_core(world):
|
||||
@@ -675,7 +675,7 @@ def get_pool_core(world):
|
||||
world.remove_from_start_inventory.append('Scarecrow Song')
|
||||
|
||||
if world.no_epona_race:
|
||||
world.multiworld.push_precollected(world.create_item('Epona'))
|
||||
world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True))
|
||||
world.remove_from_start_inventory.append('Epona')
|
||||
|
||||
if world.shuffle_smallkeys == 'vanilla':
|
||||
|
||||
@@ -2,6 +2,8 @@ from enum import Enum
|
||||
from .LocationList import location_table
|
||||
from BaseClasses import Location
|
||||
|
||||
non_indexed_location_types = {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}
|
||||
|
||||
location_id_offset = 67000
|
||||
locnames_pre_70 = {
|
||||
"Gift from Sages",
|
||||
@@ -18,7 +20,7 @@ new_name_order = sorted(location_table.keys(),
|
||||
else 0)
|
||||
|
||||
location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(new_name_order)
|
||||
if location_table[name][0] not in {'Boss', 'Event', 'Drop', 'HintStone', 'Hint'}}
|
||||
if location_table[name][0] not in non_indexed_location_types}
|
||||
|
||||
class DisableType(Enum):
|
||||
ENABLED = 0
|
||||
@@ -83,3 +85,57 @@ def LocationFactory(locations, player: int):
|
||||
return ret
|
||||
|
||||
|
||||
def build_location_name_groups() -> dict:
|
||||
|
||||
def fix_sing(t) -> tuple:
|
||||
if isinstance(t, str):
|
||||
return (t,)
|
||||
return t
|
||||
|
||||
def rename(d, k1, k2) -> None:
|
||||
d[k2] = d[k1]
|
||||
del d[k1]
|
||||
|
||||
# whoever wrote the location table didn't realize they need to add a comma to mark a singleton as a tuple
|
||||
# so we have to check types unfortunately
|
||||
tags = set()
|
||||
for v in location_table.values():
|
||||
if v[5] is not None:
|
||||
tags.update(fix_sing(v[5]))
|
||||
|
||||
sorted_tags = sorted(list(tags))
|
||||
|
||||
ret = {
|
||||
tag: {k for k, v in location_table.items()
|
||||
if v[5] is not None
|
||||
and tag in fix_sing(v[5])
|
||||
and v[0] not in non_indexed_location_types}
|
||||
for tag in sorted_tags
|
||||
}
|
||||
|
||||
# Delete tags which are a combination of other tags
|
||||
del ret['Death Mountain']
|
||||
del ret['Forest']
|
||||
del ret['Gerudo']
|
||||
del ret['Kakariko']
|
||||
del ret['Market']
|
||||
|
||||
# Delete Vanilla and MQ tags because they are just way too broad
|
||||
del ret['Vanilla']
|
||||
del ret['Master Quest']
|
||||
|
||||
rename(ret, 'Beehive', 'Beehives')
|
||||
rename(ret, 'Cow', 'Cows')
|
||||
rename(ret, 'Crate', 'Crates')
|
||||
rename(ret, 'Deku Scrub', 'Deku Scrubs')
|
||||
rename(ret, 'FlyingPot', 'Flying Pots')
|
||||
rename(ret, 'Freestanding', 'Freestanding Items')
|
||||
rename(ret, 'Pot', 'Pots')
|
||||
rename(ret, 'RupeeTower', 'Rupee Groups')
|
||||
rename(ret, 'SmallCrate', 'Small Crates')
|
||||
rename(ret, 'the Market', 'Market')
|
||||
rename(ret, 'the Graveyard', 'Graveyard')
|
||||
rename(ret, 'the Lost Woods', 'Lost Woods')
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ location_table = OrderedDict([
|
||||
("Market Night Green Rupee Crate 1", ("Crate", 0x21, (0,0,24), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
|
||||
("Market Night Green Rupee Crate 2", ("Crate", 0x21, (0,0,25), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
|
||||
("Market Night Green Rupee Crate 3", ("Crate", 0x21, (0,0,26), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
|
||||
("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("Market", "Market", "Crate"))),
|
||||
("Market Dog Lady House Crate", ("Crate", 0x35, (0,0,3), None, 'Rupees (5)', ("the Market", "Market", "Crate"))),
|
||||
("Market Guard House Child Crate", ("Crate", 0x4D, (0,0,6), None, 'Rupee (1)', ("the Market", "Market", "Crate"))),
|
||||
("Market Guard House Child Pot 1", ("Pot", 0x4D, (0,0,9), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
|
||||
("Market Guard House Child Pot 2", ("Pot", 0x4D, (0,0,10), None, 'Rupee (1)', ("the Market", "Market", "Pot"))),
|
||||
|
||||
@@ -30,7 +30,17 @@ class TrackRandomRange(Range):
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
"""Set the logic used for the generator."""
|
||||
"""Set the logic used for the generator.
|
||||
Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option.
|
||||
Glitched: Many powerful glitches expected, such as bomb hovering and clipping.
|
||||
Glitched is incompatible with the following settings:
|
||||
- All forms of entrance randomizer
|
||||
- MQ dungeons
|
||||
- Pot shuffle
|
||||
- Freestanding item shuffle
|
||||
- Crate shuffle
|
||||
- Beehive shuffle
|
||||
No Logic: No logic is used when placing items. Not recommended for most players."""
|
||||
display_name = "Logic Rules"
|
||||
option_glitchless = 0
|
||||
option_glitched = 1
|
||||
@@ -38,12 +48,16 @@ class Logic(Choice):
|
||||
|
||||
|
||||
class NightTokens(Toggle):
|
||||
"""Nighttime skulltulas will logically require Sun's Song."""
|
||||
"""When enabled, nighttime skulltulas logically require Sun's Song."""
|
||||
display_name = "Nighttime Skulltulas Expect Sun's Song"
|
||||
|
||||
|
||||
class Forest(Choice):
|
||||
"""Set the state of Kokiri Forest and the path to Deku Tree."""
|
||||
"""Set the state of Kokiri Forest and the path to Deku Tree.
|
||||
Open: Neither the forest exit nor the path to Deku Tree is blocked.
|
||||
Closed Deku: The forest exit is not blocked; the path to Deku Tree requires Kokiri Sword and Deku Shield.
|
||||
Closed: Path to Deku Tree requires sword and shield. The forest exit is blocked until Deku Tree is beaten.
|
||||
Closed forest will force child start, and becomes Closed Deku if interior entrances, overworld entrances, warp songs, or random spawn positions are enabled."""
|
||||
display_name = "Forest"
|
||||
option_open = 0
|
||||
option_closed_deku = 1
|
||||
@@ -53,7 +67,10 @@ class Forest(Choice):
|
||||
|
||||
|
||||
class Gate(Choice):
|
||||
"""Set the state of the Kakariko Village gate."""
|
||||
"""Set the state of the Kakariko Village gate for child. The gate is always open as adult.
|
||||
Open: The gate starts open. Happy Mask Shop opens upon receiving Zelda's Letter.
|
||||
Zelda: The gate and Mask Shop open upon receiving Zelda's Letter, without needing to show it to the guard.
|
||||
Closed: Vanilla behavior; the gate and Mask Shop open upon showing Zelda's Letter to the gate guard."""
|
||||
display_name = "Kakariko Gate"
|
||||
option_open = 0
|
||||
option_zelda = 1
|
||||
@@ -61,12 +78,15 @@ class Gate(Choice):
|
||||
|
||||
|
||||
class DoorOfTime(DefaultOnToggle):
|
||||
"""Open the Door of Time by default, without the Song of Time."""
|
||||
"""When enabled, the Door of Time starts opened, without needing Song of Time."""
|
||||
display_name = "Open Door of Time"
|
||||
|
||||
|
||||
class Fountain(Choice):
|
||||
"""Set the state of King Zora, blocking the way to Zora's Fountain."""
|
||||
"""Set the state of King Zora, blocking the way to Zora's Fountain.
|
||||
Open: King Zora starts moved as both ages. Ruto's Letter is removed.
|
||||
Adult: King Zora must be moved as child, but is always moved for adult.
|
||||
Closed: Vanilla behavior; King Zora must be shown Ruto's Letter as child to move him as both ages."""
|
||||
display_name = "Zora's Fountain"
|
||||
option_open = 0
|
||||
option_adult = 1
|
||||
@@ -75,7 +95,10 @@ class Fountain(Choice):
|
||||
|
||||
|
||||
class Fortress(Choice):
|
||||
"""Set the requirements for access to Gerudo Fortress."""
|
||||
"""Set the requirements for access to Gerudo Fortress.
|
||||
Normal: Vanilla behavior; all four carpenters must be rescued.
|
||||
Fast: Only one carpenter must be rescued, which is the one in the bottom-left of the fortress.
|
||||
Open: The Gerudo Valley bridge starts repaired. Gerudo Membership Card is given to start if not shuffled."""
|
||||
display_name = "Gerudo Fortress"
|
||||
option_normal = 0
|
||||
option_fast = 1
|
||||
@@ -84,7 +107,14 @@ class Fortress(Choice):
|
||||
|
||||
|
||||
class Bridge(Choice):
|
||||
"""Set the requirements for the Rainbow Bridge."""
|
||||
"""Set the requirements for the Rainbow Bridge.
|
||||
Open: The bridge is always present.
|
||||
Vanilla: Bridge requires Shadow Medallion, Spirit Medallion, and Light Arrows.
|
||||
Stones: Bridge requires a configurable amount of Spiritual Stones.
|
||||
Medallions: Bridge requires a configurable amount of medallions.
|
||||
Dungeons: Bridge requires a configurable amount of rewards (stones + medallions).
|
||||
Tokens: Bridge requires a configurable amount of gold skulltula tokens.
|
||||
Hearts: Bridge requires a configurable amount of hearts."""
|
||||
display_name = "Rainbow Bridge Requirement"
|
||||
option_open = 0
|
||||
option_vanilla = 1
|
||||
@@ -122,8 +152,9 @@ class StartingAge(Choice):
|
||||
|
||||
|
||||
class InteriorEntrances(Choice):
|
||||
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House,
|
||||
Temple of Time, and Kak potion shop."""
|
||||
"""Shuffles interior entrances.
|
||||
Simple: Houses and Great Fairies are shuffled.
|
||||
All: In addition to Simple, includes Windmill, Link's House, Temple of Time, and the Kakariko potion shop."""
|
||||
display_name = "Shuffle Interior Entrances"
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
@@ -137,7 +168,9 @@ class GrottoEntrances(Toggle):
|
||||
|
||||
|
||||
class DungeonEntrances(Choice):
|
||||
"""Shuffles dungeon entrances. Opens Deku, Fire and BotW to both ages. "All" includes Ganon's Castle."""
|
||||
"""Shuffles dungeon entrances. When enabled, both ages will have access to Fire Temple, Bottom of the Well, and Deku Tree.
|
||||
Simple: Shuffle dungeon entrances except for Ganon's Castle.
|
||||
All: Include Ganon's Castle as well."""
|
||||
display_name = "Shuffle Dungeon Entrances"
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
@@ -146,7 +179,9 @@ class DungeonEntrances(Choice):
|
||||
|
||||
|
||||
class BossEntrances(Choice):
|
||||
"""Shuffles boss entrances. "Limited" prevents age-mixing of bosses."""
|
||||
"""Shuffles boss entrances.
|
||||
Limited: Bosses will be limited to the ages that typically fight them.
|
||||
Full: Bosses may be fought as different ages than usual. Child can defeat Phantom Ganon and Bongo Bongo."""
|
||||
display_name = "Shuffle Boss Entrances"
|
||||
option_off = 0
|
||||
option_limited = 1
|
||||
@@ -178,19 +213,19 @@ class SpawnPositions(Choice):
|
||||
alias_true = 3
|
||||
|
||||
|
||||
class MixEntrancePools(Choice):
|
||||
"""Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
|
||||
mixes them in."""
|
||||
display_name = "Mix Entrance Pools"
|
||||
option_off = 0
|
||||
option_indoor = 1
|
||||
option_all = 2
|
||||
# class MixEntrancePools(Choice):
|
||||
# """Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all"
|
||||
# mixes them in."""
|
||||
# display_name = "Mix Entrance Pools"
|
||||
# option_off = 0
|
||||
# option_indoor = 1
|
||||
# option_all = 2
|
||||
|
||||
|
||||
class DecoupleEntrances(Toggle):
|
||||
"""Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
|
||||
overworld is shuffled."""
|
||||
display_name = "Decouple Entrances"
|
||||
# class DecoupleEntrances(Toggle):
|
||||
# """Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if
|
||||
# overworld is shuffled."""
|
||||
# display_name = "Decouple Entrances"
|
||||
|
||||
|
||||
class TriforceHunt(Toggle):
|
||||
@@ -216,13 +251,17 @@ class ExtraTriforces(Range):
|
||||
|
||||
|
||||
class LogicalChus(Toggle):
|
||||
"""Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell
|
||||
refills; bombchus open Bombchu Bowling."""
|
||||
"""Bombchus are properly considered in logic.
|
||||
The first found pack will always have 20 chus.
|
||||
Kokiri Shop and Bazaar will sell refills at reduced cost.
|
||||
Bombchus open Bombchu Bowling."""
|
||||
display_name = "Bombchus Considered in Logic"
|
||||
|
||||
|
||||
class DungeonShortcuts(Choice):
|
||||
"""Shortcuts to dungeon bosses are available without any requirements."""
|
||||
"""Shortcuts to dungeon bosses are available without any requirements.
|
||||
If enabled, this will impact the logic of dungeons where shortcuts are available.
|
||||
Choice: Use the option "dungeon_shortcuts_list" to choose shortcuts."""
|
||||
display_name = "Dungeon Boss Shortcuts Mode"
|
||||
option_off = 0
|
||||
option_choice = 1
|
||||
@@ -246,7 +285,11 @@ class DungeonShortcutsList(OptionSet):
|
||||
|
||||
|
||||
class MQDungeons(Choice):
|
||||
"""Choose between vanilla and Master Quest dungeon layouts."""
|
||||
"""Choose between vanilla and Master Quest dungeon layouts.
|
||||
Vanilla: All layouts are vanilla.
|
||||
MQ: All layouts are Master Quest.
|
||||
Specific: Use the option "mq_dungeons_list" to choose which dungeons are MQ.
|
||||
Count: Use the option "mq_dungeons_count" to choose a number of random dungeons as MQ."""
|
||||
display_name = "MQ Dungeon Mode"
|
||||
option_vanilla = 0
|
||||
option_mq = 1
|
||||
@@ -255,7 +298,7 @@ class MQDungeons(Choice):
|
||||
|
||||
|
||||
class MQDungeonList(OptionSet):
|
||||
"""Chosen dungeons to be MQ layout."""
|
||||
"""With MQ dungeons as Specific: chosen dungeons to be MQ layout."""
|
||||
display_name = "MQ Dungeon List"
|
||||
valid_keys = {
|
||||
"Deku Tree",
|
||||
@@ -274,41 +317,41 @@ class MQDungeonList(OptionSet):
|
||||
|
||||
|
||||
class MQDungeonCount(TrackRandomRange):
|
||||
"""Number of MQ dungeons, chosen randomly."""
|
||||
"""With MQ dungeons as Count: number of randomly-selected dungeons to be MQ layout."""
|
||||
display_name = "MQ Dungeon Count"
|
||||
range_start = 0
|
||||
range_end = 12
|
||||
default = 0
|
||||
|
||||
|
||||
class EmptyDungeons(Choice):
|
||||
"""Pre-completed dungeons are barren and rewards are given for free."""
|
||||
display_name = "Pre-completed Dungeons Mode"
|
||||
option_none = 0
|
||||
option_specific = 1
|
||||
option_count = 2
|
||||
# class EmptyDungeons(Choice):
|
||||
# """Pre-completed dungeons are barren and rewards are given for free."""
|
||||
# display_name = "Pre-completed Dungeons Mode"
|
||||
# option_none = 0
|
||||
# option_specific = 1
|
||||
# option_count = 2
|
||||
|
||||
|
||||
class EmptyDungeonList(OptionSet):
|
||||
"""Chosen dungeons to be pre-completed."""
|
||||
display_name = "Pre-completed Dungeon List"
|
||||
valid_keys = {
|
||||
"Deku Tree",
|
||||
"Dodongo's Cavern",
|
||||
"Jabu Jabu's Belly",
|
||||
"Forest Temple",
|
||||
"Fire Temple",
|
||||
"Water Temple",
|
||||
"Shadow Temple",
|
||||
"Spirit Temple",
|
||||
}
|
||||
# class EmptyDungeonList(OptionSet):
|
||||
# """Chosen dungeons to be pre-completed."""
|
||||
# display_name = "Pre-completed Dungeon List"
|
||||
# valid_keys = {
|
||||
# "Deku Tree",
|
||||
# "Dodongo's Cavern",
|
||||
# "Jabu Jabu's Belly",
|
||||
# "Forest Temple",
|
||||
# "Fire Temple",
|
||||
# "Water Temple",
|
||||
# "Shadow Temple",
|
||||
# "Spirit Temple",
|
||||
# }
|
||||
|
||||
|
||||
class EmptyDungeonCount(Range):
|
||||
display_name = "Pre-completed Dungeon Count"
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
default = 2
|
||||
# class EmptyDungeonCount(Range):
|
||||
# display_name = "Pre-completed Dungeon Count"
|
||||
# range_start = 1
|
||||
# range_end = 8
|
||||
# default = 2
|
||||
|
||||
|
||||
world_options: typing.Dict[str, type(Option)] = {
|
||||
@@ -341,59 +384,8 @@ world_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
|
||||
|
||||
# class LacsCondition(Choice):
|
||||
# """Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
|
||||
# display_name = "Light Arrow Cutscene Requirement"
|
||||
# option_vanilla = 0
|
||||
# option_stones = 1
|
||||
# option_medallions = 2
|
||||
# option_dungeons = 3
|
||||
# option_tokens = 4
|
||||
|
||||
|
||||
# class LacsStones(Range):
|
||||
# """Set the number of Spiritual Stones required for LACS."""
|
||||
# display_name = "Spiritual Stones Required for LACS"
|
||||
# range_start = 0
|
||||
# range_end = 3
|
||||
# default = 3
|
||||
|
||||
|
||||
# class LacsMedallions(Range):
|
||||
# """Set the number of medallions required for LACS."""
|
||||
# display_name = "Medallions Required for LACS"
|
||||
# range_start = 0
|
||||
# range_end = 6
|
||||
# default = 6
|
||||
|
||||
|
||||
# class LacsRewards(Range):
|
||||
# """Set the number of dungeon rewards required for LACS."""
|
||||
# display_name = "Dungeon Rewards Required for LACS"
|
||||
# range_start = 0
|
||||
# range_end = 9
|
||||
# default = 9
|
||||
|
||||
|
||||
# class LacsTokens(Range):
|
||||
# """Set the number of Gold Skulltula Tokens required for LACS."""
|
||||
# display_name = "Tokens Required for LACS"
|
||||
# range_start = 0
|
||||
# range_end = 100
|
||||
# default = 40
|
||||
|
||||
|
||||
# lacs_options: typing.Dict[str, type(Option)] = {
|
||||
# "lacs_condition": LacsCondition,
|
||||
# "lacs_stones": LacsStones,
|
||||
# "lacs_medallions": LacsMedallions,
|
||||
# "lacs_rewards": LacsRewards,
|
||||
# "lacs_tokens": LacsTokens,
|
||||
# }
|
||||
|
||||
|
||||
class BridgeStones(Range):
|
||||
"""Set the number of Spiritual Stones required for the rainbow bridge."""
|
||||
"""With Stones bridge: set the number of Spiritual Stones required."""
|
||||
display_name = "Spiritual Stones Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 3
|
||||
@@ -401,7 +393,7 @@ class BridgeStones(Range):
|
||||
|
||||
|
||||
class BridgeMedallions(Range):
|
||||
"""Set the number of medallions required for the rainbow bridge."""
|
||||
"""With Medallions bridge: set the number of medallions required."""
|
||||
display_name = "Medallions Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
@@ -409,7 +401,7 @@ class BridgeMedallions(Range):
|
||||
|
||||
|
||||
class BridgeRewards(Range):
|
||||
"""Set the number of dungeon rewards required for the rainbow bridge."""
|
||||
"""With Dungeons bridge: set the number of dungeon rewards required."""
|
||||
display_name = "Dungeon Rewards Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 9
|
||||
@@ -417,7 +409,7 @@ class BridgeRewards(Range):
|
||||
|
||||
|
||||
class BridgeTokens(Range):
|
||||
"""Set the number of Gold Skulltula Tokens required for the rainbow bridge."""
|
||||
"""With Tokens bridge: set the number of Gold Skulltula Tokens required."""
|
||||
display_name = "Tokens Required for Bridge"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
@@ -425,7 +417,7 @@ class BridgeTokens(Range):
|
||||
|
||||
|
||||
class BridgeHearts(Range):
|
||||
"""Set the number of hearts required for the rainbow bridge."""
|
||||
"""With Hearts bridge: set the number of hearts required."""
|
||||
display_name = "Hearts Required for Bridge"
|
||||
range_start = 4
|
||||
range_end = 20
|
||||
@@ -442,7 +434,15 @@ bridge_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
|
||||
class SongShuffle(Choice):
|
||||
"""Set where songs can appear."""
|
||||
"""Set where songs can appear.
|
||||
Song: Songs are shuffled into other song locations.
|
||||
Dungeon: Songs are placed into end-of-dungeon locations:
|
||||
- The 8 boss heart containers
|
||||
- Sheik in Ice Cavern
|
||||
- Lens of Truth chest in Bottom of the Well
|
||||
- Ice Arrows chest in Gerudo Training Ground
|
||||
- Impa at Hyrule Castle
|
||||
Any: Songs can appear anywhere in the multiworld."""
|
||||
display_name = "Shuffle Songs"
|
||||
option_song = 0
|
||||
option_dungeon = 1
|
||||
@@ -450,8 +450,10 @@ class SongShuffle(Choice):
|
||||
|
||||
|
||||
class ShopShuffle(Choice):
|
||||
"""Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop;
|
||||
"random_number" randomizes the value for each shop. """
|
||||
"""Randomizes shop contents.
|
||||
Off: Shops are not randomized at all.
|
||||
Fixed Number: Shop contents are shuffled, and a specific number of multiworld locations exist in each shop, controlled by the "shop_slots" option.
|
||||
Random Number: Same as Fixed Number, but the number of locations per shop is random and may differ between shops."""
|
||||
display_name = "Shopsanity"
|
||||
option_off = 0
|
||||
option_fixed_number = 1
|
||||
@@ -459,15 +461,20 @@ class ShopShuffle(Choice):
|
||||
|
||||
|
||||
class ShopSlots(Range):
|
||||
"""Number of items per shop to be randomized into the main itempool.
|
||||
Only active if Shopsanity is set to "fixed_number." """
|
||||
"""With Shopsanity fixed number: quantity of multiworld locations per shop to be randomized."""
|
||||
display_name = "Shuffled Shop Slots"
|
||||
range_start = 0
|
||||
range_end = 4
|
||||
|
||||
|
||||
class ShopPrices(Choice):
|
||||
"""Controls prices of shop items. "Normal" is a distribution from 0 to 300. "X Wallet" requires that wallet at max. "Affordable" is always 10 rupees."""
|
||||
"""Controls prices of shop locations.
|
||||
Normal: Balanced distribution from 0 to 300.
|
||||
Affordable: Every shop location costs 10 rupees.
|
||||
Starting Wallet: Prices capped at 99 rupees.
|
||||
Adult's Wallet: Prices capped at 200 rupees.
|
||||
Giant's Wallet: Prices capped at 500 rupees.
|
||||
Tycoon's Wallet: Prices capped at 999 rupees."""
|
||||
display_name = "Shopsanity Prices"
|
||||
option_normal = 0
|
||||
option_affordable = 1
|
||||
@@ -478,7 +485,10 @@ class ShopPrices(Choice):
|
||||
|
||||
|
||||
class TokenShuffle(Choice):
|
||||
"""Token rewards from Gold Skulltulas are shuffled into the pool."""
|
||||
"""Token rewards from Gold Skulltulas can be shuffled into the pool.
|
||||
Dungeons: Only skulltulas in dungeons are shuffled.
|
||||
Overworld: Only skulltulas on the overworld (all skulltulas not in dungeons) are shuffled.
|
||||
All: Every skulltula is shuffled."""
|
||||
display_name = "Tokensanity"
|
||||
option_off = 0
|
||||
option_dungeons = 1
|
||||
@@ -487,7 +497,11 @@ class TokenShuffle(Choice):
|
||||
|
||||
|
||||
class ScrubShuffle(Choice):
|
||||
"""Shuffle the items sold by Business Scrubs, and set the prices."""
|
||||
"""Shuffle the items sold by Business Scrubs, and set the prices.
|
||||
Off: Only the three business scrubs that sell one-time upgrades in vanilla will have items at their vanilla prices.
|
||||
Low/"Affordable": All scrub prices are 10 rupees.
|
||||
Regular/"Expensive": All scrub prices are vanilla.
|
||||
Random Prices: All scrub prices are randomized between 0 and 99 rupees."""
|
||||
display_name = "Scrub Shuffle"
|
||||
option_off = 0
|
||||
option_low = 1
|
||||
@@ -513,7 +527,11 @@ class ShuffleOcarinas(Toggle):
|
||||
|
||||
|
||||
class ShuffleChildTrade(Choice):
|
||||
"""Controls the behavior of the start of the child trade quest."""
|
||||
"""Controls the behavior of the start of the child trade quest.
|
||||
Vanilla: Malon will give you the Weird Egg at Hyrule Castle.
|
||||
Shuffle: Malon will give you a random item, and the Weird Egg is shuffled.
|
||||
Skip Child Zelda: The game starts with Zelda already met, Zelda's Letter obtained, and the item from Impa obtained.
|
||||
"""
|
||||
display_name = "Shuffle Child Trade Item"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
@@ -538,30 +556,39 @@ class ShuffleMedigoronCarpet(Toggle):
|
||||
|
||||
|
||||
class ShuffleFreestanding(Choice):
|
||||
"""Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot."""
|
||||
"""Shuffles freestanding rupees, recovery hearts, Shadow Temple Spinning Pots, and Goron Pot drops.
|
||||
Dungeons: Only freestanding items in dungeons are shuffled.
|
||||
Overworld: Only freestanding items in the overworld are shuffled.
|
||||
All: All freestanding items are shuffled."""
|
||||
display_name = "Shuffle Rupees & Hearts"
|
||||
option_off = 0
|
||||
option_all = 1
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_dungeons = 3
|
||||
option_all = 3
|
||||
|
||||
|
||||
class ShufflePots(Choice):
|
||||
"""Shuffles pots and flying pots which normally contain an item."""
|
||||
"""Shuffles pots and flying pots which normally contain an item.
|
||||
Dungeons: Only pots in dungeons are shuffled.
|
||||
Overworld: Only pots in the overworld are shuffled.
|
||||
All: All pots are shuffled."""
|
||||
display_name = "Shuffle Pots"
|
||||
option_off = 0
|
||||
option_all = 1
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_dungeons = 3
|
||||
option_all = 3
|
||||
|
||||
|
||||
class ShuffleCrates(Choice):
|
||||
"""Shuffles large and small crates containing an item."""
|
||||
"""Shuffles large and small crates containing an item.
|
||||
Dungeons: Only crates in dungeons are shuffled.
|
||||
Overworld: Only crates in the overworld are shuffled.
|
||||
All: All crates are shuffled."""
|
||||
display_name = "Shuffle Crates"
|
||||
option_off = 0
|
||||
option_all = 1
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_dungeons = 3
|
||||
option_all = 3
|
||||
|
||||
|
||||
class ShuffleBeehives(Toggle):
|
||||
@@ -597,72 +624,113 @@ shuffle_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
|
||||
class ShuffleMapCompass(Choice):
|
||||
"""Control where to shuffle dungeon maps and compasses."""
|
||||
"""Control where to shuffle dungeon maps and compasses.
|
||||
Remove: There will be no maps or compasses in the itempool.
|
||||
Startwith: You start with all maps and compasses.
|
||||
Vanilla: Maps and compasses remain vanilla.
|
||||
Dungeon: Maps and compasses are shuffled within their original dungeon.
|
||||
Regional: Maps and compasses are shuffled only in regions near the original dungeon.
|
||||
Overworld: Maps and compasses are shuffled locally outside of dungeons.
|
||||
Any Dungeon: Maps and compasses are shuffled locally in any dungeon.
|
||||
Keysanity: Maps and compasses can be anywhere in the multiworld."""
|
||||
display_name = "Maps & Compasses"
|
||||
option_remove = 0
|
||||
option_startwith = 1
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
option_regional = 7
|
||||
option_regional = 4
|
||||
option_overworld = 5
|
||||
option_any_dungeon = 6
|
||||
option_keysanity = 7
|
||||
default = 1
|
||||
alias_anywhere = 6
|
||||
alias_anywhere = 7
|
||||
|
||||
|
||||
class ShuffleKeys(Choice):
|
||||
"""Control where to shuffle dungeon small keys."""
|
||||
"""Control where to shuffle dungeon small keys.
|
||||
Remove/"Keysy": There will be no small keys in the itempool. All small key doors are automatically unlocked.
|
||||
Vanilla: Small keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
|
||||
Dungeon: Small keys are shuffled within their original dungeon.
|
||||
Regional: Small keys are shuffled only in regions near the original dungeon.
|
||||
Overworld: Small keys are shuffled locally outside of dungeons.
|
||||
Any Dungeon: Small keys are shuffled locally in any dungeon.
|
||||
Keysanity: Small keys can be anywhere in the multiworld."""
|
||||
display_name = "Small Keys"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
option_regional = 7
|
||||
option_regional = 4
|
||||
option_overworld = 5
|
||||
option_any_dungeon = 6
|
||||
option_keysanity = 7
|
||||
default = 3
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
alias_anywhere = 7
|
||||
|
||||
|
||||
class ShuffleGerudoKeys(Choice):
|
||||
"""Control where to shuffle the Thieves' Hideout small keys."""
|
||||
"""Control where to shuffle the Thieves' Hideout small keys.
|
||||
Vanilla: Hideout keys remain vanilla.
|
||||
Regional: Hideout keys are shuffled only in the Gerudo Valley/Desert Colossus area.
|
||||
Overworld: Hideout keys are shuffled locally outside of dungeons.
|
||||
Any Dungeon: Hideout keys are shuffled locally in any dungeon.
|
||||
Keysanity: Hideout keys can be anywhere in the multiworld."""
|
||||
display_name = "Thieves' Hideout Keys"
|
||||
option_vanilla = 0
|
||||
option_overworld = 1
|
||||
option_any_dungeon = 2
|
||||
option_keysanity = 3
|
||||
option_regional = 4
|
||||
alias_anywhere = 3
|
||||
option_regional = 1
|
||||
option_overworld = 2
|
||||
option_any_dungeon = 3
|
||||
option_keysanity = 4
|
||||
alias_anywhere = 4
|
||||
|
||||
|
||||
class ShuffleBossKeys(Choice):
|
||||
"""Control where to shuffle boss keys, except the Ganon's Castle Boss Key."""
|
||||
"""Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
|
||||
Remove/"Keysy": There will be no boss keys in the itempool. All boss key doors are automatically unlocked.
|
||||
Vanilla: Boss keys remain vanilla. You may start with extra small keys in some dungeons to prevent softlocks.
|
||||
Dungeon: Boss keys are shuffled within their original dungeon.
|
||||
Regional: Boss keys are shuffled only in regions near the original dungeon.
|
||||
Overworld: Boss keys are shuffled locally outside of dungeons.
|
||||
Any Dungeon: Boss keys are shuffled locally in any dungeon.
|
||||
Keysanity: Boss keys can be anywhere in the multiworld."""
|
||||
display_name = "Boss Keys"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
option_regional = 7
|
||||
option_regional = 4
|
||||
option_overworld = 5
|
||||
option_any_dungeon = 6
|
||||
option_keysanity = 7
|
||||
default = 3
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
alias_anywhere = 7
|
||||
|
||||
|
||||
class ShuffleGanonBK(Choice):
|
||||
"""Control how to shuffle the Ganon's Castle Boss Key."""
|
||||
"""Control how to shuffle the Ganon's Castle Boss Key (GCBK).
|
||||
Remove: GCBK is removed, and the boss key door is automatically unlocked.
|
||||
Vanilla: GCBK remains vanilla.
|
||||
Dungeon: GCBK is shuffled within its original dungeon.
|
||||
Regional: GCBK is shuffled only in Hyrule Field, Market, and Hyrule Castle areas.
|
||||
Overworld: GCBK is shuffled locally outside of dungeons.
|
||||
Any Dungeon: GCBK is shuffled locally in any dungeon.
|
||||
Keysanity: GCBK can be anywhere in the multiworld.
|
||||
On LACS: GCBK is on the Light Arrow Cutscene, which requires Shadow and Spirit Medallions.
|
||||
Stones: GCBK will be awarded when reaching the target number of Spiritual Stones.
|
||||
Medallions: GCBK will be awarded when reaching the target number of medallions.
|
||||
Dungeons: GCBK will be awarded when reaching the target number of dungeon rewards.
|
||||
Tokens: GCBK will be awarded when reaching the target number of Gold Skulltula Tokens.
|
||||
Hearts: GCBK will be awarded when reaching the target number of hearts.
|
||||
"""
|
||||
display_name = "Ganon's Boss Key"
|
||||
option_remove = 0
|
||||
option_vanilla = 2
|
||||
option_dungeon = 3
|
||||
option_overworld = 4
|
||||
option_any_dungeon = 5
|
||||
option_keysanity = 6
|
||||
option_on_lacs = 7
|
||||
option_regional = 8
|
||||
option_regional = 4
|
||||
option_overworld = 5
|
||||
option_any_dungeon = 6
|
||||
option_keysanity = 7
|
||||
option_on_lacs = 8
|
||||
option_stones = 9
|
||||
option_medallions = 10
|
||||
option_dungeons = 11
|
||||
@@ -670,7 +738,7 @@ class ShuffleGanonBK(Choice):
|
||||
option_hearts = 13
|
||||
default = 0
|
||||
alias_keysy = 0
|
||||
alias_anywhere = 6
|
||||
alias_anywhere = 7
|
||||
|
||||
|
||||
class EnhanceMC(Toggle):
|
||||
@@ -679,7 +747,7 @@ class EnhanceMC(Toggle):
|
||||
|
||||
|
||||
class GanonBKMedallions(Range):
|
||||
"""Set how many medallions are required to receive Ganon BK."""
|
||||
"""With medallions GCBK: set how many medallions are required to receive GCBK."""
|
||||
display_name = "Medallions Required for Ganon's BK"
|
||||
range_start = 1
|
||||
range_end = 6
|
||||
@@ -687,7 +755,7 @@ class GanonBKMedallions(Range):
|
||||
|
||||
|
||||
class GanonBKStones(Range):
|
||||
"""Set how many Spiritual Stones are required to receive Ganon BK."""
|
||||
"""With stones GCBK: set how many Spiritual Stones are required to receive GCBK."""
|
||||
display_name = "Spiritual Stones Required for Ganon's BK"
|
||||
range_start = 1
|
||||
range_end = 3
|
||||
@@ -695,7 +763,7 @@ class GanonBKStones(Range):
|
||||
|
||||
|
||||
class GanonBKRewards(Range):
|
||||
"""Set how many dungeon rewards are required to receive Ganon BK."""
|
||||
"""With dungeons GCBK: set how many dungeon rewards are required to receive GCBK."""
|
||||
display_name = "Dungeon Rewards Required for Ganon's BK"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
@@ -703,7 +771,7 @@ class GanonBKRewards(Range):
|
||||
|
||||
|
||||
class GanonBKTokens(Range):
|
||||
"""Set how many Gold Skulltula Tokens are required to receive Ganon BK."""
|
||||
"""With tokens GCBK: set how many Gold Skulltula Tokens are required to receive GCBK."""
|
||||
display_name = "Tokens Required for Ganon's BK"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
@@ -711,7 +779,7 @@ class GanonBKTokens(Range):
|
||||
|
||||
|
||||
class GanonBKHearts(Range):
|
||||
"""Set how many hearts are required to receive Ganon BK."""
|
||||
"""With hearts GCBK: set how many hearts are required to receive GCBK."""
|
||||
display_name = "Hearts Required for Ganon's BK"
|
||||
range_start = 4
|
||||
range_end = 20
|
||||
@@ -719,7 +787,9 @@ class GanonBKHearts(Range):
|
||||
|
||||
|
||||
class KeyRings(Choice):
|
||||
"""Dungeons have all small keys found at once, rather than individually."""
|
||||
"""A key ring grants all dungeon small keys at once, rather than individually.
|
||||
Choose: Use the option "key_rings_list" to choose which dungeons have key rings.
|
||||
All: All dungeons have key rings instead of small keys."""
|
||||
display_name = "Key Rings Mode"
|
||||
option_off = 0
|
||||
option_choose = 1
|
||||
@@ -728,7 +798,7 @@ class KeyRings(Choice):
|
||||
|
||||
|
||||
class KeyRingList(OptionSet):
|
||||
"""Select areas with keyrings rather than individual small keys."""
|
||||
"""With key rings as Choose: select areas with key rings rather than individual small keys."""
|
||||
display_name = "Key Ring Areas"
|
||||
valid_keys = {
|
||||
"Thieves' Hideout",
|
||||
@@ -828,7 +898,8 @@ class BigPoeCount(Range):
|
||||
|
||||
|
||||
class FAETorchCount(Range):
|
||||
"""Number of lit torches required to open Shadow Temple."""
|
||||
"""Number of lit torches required to open Shadow Temple.
|
||||
Does not affect logic; use the trick Shadow Temple Entry with Fire Arrows if desired."""
|
||||
display_name = "Fire Arrow Entry Torch Count"
|
||||
range_start = 1
|
||||
range_end = 24
|
||||
@@ -853,7 +924,11 @@ timesavers_options: typing.Dict[str, type(Option)] = {
|
||||
|
||||
|
||||
class CorrectChestAppearance(Choice):
|
||||
"""Changes chest textures and/or sizes to match their contents. "Classic" is the old behavior of CSMC."""
|
||||
"""Changes chest textures and/or sizes to match their contents.
|
||||
Off: All chests have their vanilla size/appearance.
|
||||
Textures: Chest textures reflect their contents.
|
||||
Both: Like Textures, but progression items and boss keys get big chests, and other items get small chests.
|
||||
Classic: Old behavior of CSMC; textures distinguish keys from non-keys, and size distinguishes importance."""
|
||||
display_name = "Chest Appearance Matches Contents"
|
||||
option_off = 0
|
||||
option_textures = 1
|
||||
@@ -872,15 +947,24 @@ class InvisibleChests(Toggle):
|
||||
|
||||
|
||||
class CorrectPotCrateAppearance(Choice):
|
||||
"""Unchecked pots and crates have a different texture; unchecked beehives will wiggle. With textures_content, pots and crates have an appearance based on their contents; with textures_unchecked, all unchecked pots/crates have the same appearance."""
|
||||
"""Changes the appearance of pots, crates, and beehives that contain items.
|
||||
Off: Vanilla appearance for all containers.
|
||||
Textures (Content): Unchecked pots and crates have a texture reflecting their contents. Unchecked beehives with progression items will wiggle.
|
||||
Textures (Unchecked): Unchecked pots and crates are golden. Unchecked beehives will wiggle.
|
||||
"""
|
||||
display_name = "Pot, Crate, and Beehive Appearance"
|
||||
option_off = 0
|
||||
option_textures_content = 1
|
||||
option_textures_unchecked = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class Hints(Choice):
|
||||
"""Gossip Stones can give hints about item locations."""
|
||||
"""Gossip Stones can give hints about item locations.
|
||||
None: Gossip Stones do not give hints.
|
||||
Mask: Gossip Stones give hints with Mask of Truth.
|
||||
Agony: Gossip Stones give hints wtih Stone of Agony.
|
||||
Always: Gossip Stones always give hints."""
|
||||
display_name = "Gossip Stones"
|
||||
option_none = 0
|
||||
option_mask = 1
|
||||
@@ -895,7 +979,9 @@ class MiscHints(DefaultOnToggle):
|
||||
|
||||
|
||||
class HintDistribution(Choice):
|
||||
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
|
||||
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
|
||||
Detailed documentation on hint distributions can be found on the Archipelago GitHub or OoTRandomizer.com.
|
||||
The Async hint distribution is intended for async multiworlds. It removes Way of the Hero hints to improve generation times, since they are not very useful in asyncs."""
|
||||
display_name = "Hint Distribution"
|
||||
option_balanced = 0
|
||||
option_ddr = 1
|
||||
@@ -907,10 +993,13 @@ class HintDistribution(Choice):
|
||||
option_useless = 7
|
||||
option_very_strong = 8
|
||||
option_async = 9
|
||||
default = 9
|
||||
|
||||
|
||||
class TextShuffle(Choice):
|
||||
"""Randomizes text in the game for comedic effect."""
|
||||
"""Randomizes text in the game for comedic effect.
|
||||
Except Hints: does not randomize important text such as hints, small/boss key information, and item prices.
|
||||
Complete: randomizes every textbox, including the useful ones."""
|
||||
display_name = "Text Shuffle"
|
||||
option_none = 0
|
||||
option_except_hints = 1
|
||||
@@ -946,7 +1035,8 @@ class HeroMode(Toggle):
|
||||
|
||||
|
||||
class StartingToD(Choice):
|
||||
"""Change the starting time of day."""
|
||||
"""Change the starting time of day.
|
||||
Daytime starts at Sunrise and ends at Sunset. Default is between Morning and Noon."""
|
||||
display_name = "Starting Time of Day"
|
||||
option_default = 0
|
||||
option_sunrise = 1
|
||||
@@ -999,7 +1089,11 @@ misc_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
|
||||
class ItemPoolValue(Choice):
|
||||
"""Changes the number of items available in the game."""
|
||||
"""Changes the number of items available in the game.
|
||||
Plentiful: One extra copy of every major item.
|
||||
Balanced: Original item pool.
|
||||
Scarce: Extra copies of major items are removed. Heart containers are removed.
|
||||
Minimal: All major item upgrades not used for locations are removed. All health is removed."""
|
||||
display_name = "Item Pool"
|
||||
option_plentiful = 0
|
||||
option_balanced = 1
|
||||
@@ -1009,7 +1103,12 @@ class ItemPoolValue(Choice):
|
||||
|
||||
|
||||
class IceTraps(Choice):
|
||||
"""Adds ice traps to the item pool."""
|
||||
"""Adds ice traps to the item pool.
|
||||
Off: All ice traps are removed.
|
||||
Normal: The vanilla quantity of ice traps are placed.
|
||||
On/"Extra": There is a chance for some extra ice traps to be placed.
|
||||
Mayhem: All added junk items are ice traps.
|
||||
Onslaught: All junk items are replaced by ice traps, even those in the base pool."""
|
||||
display_name = "Ice Traps"
|
||||
option_off = 0
|
||||
option_normal = 1
|
||||
@@ -1021,34 +1120,27 @@ class IceTraps(Choice):
|
||||
|
||||
|
||||
class IceTrapVisual(Choice):
|
||||
"""Changes the appearance of ice traps as freestanding items."""
|
||||
display_name = "Ice Trap Appearance"
|
||||
"""Changes the appearance of traps, including other games' traps, as freestanding items."""
|
||||
display_name = "Trap Appearance"
|
||||
option_major_only = 0
|
||||
option_junk_only = 1
|
||||
option_anything = 2
|
||||
|
||||
|
||||
class AdultTradeStart(OptionSet):
|
||||
"""Choose the items that can appear to start the adult trade sequence. By default it is Claim Check only."""
|
||||
display_name = "Adult Trade Sequence Items"
|
||||
default = {"Claim Check"}
|
||||
valid_keys = {
|
||||
"Pocket Egg",
|
||||
"Pocket Cucco",
|
||||
"Cojiro",
|
||||
"Odd Mushroom",
|
||||
"Poachers Saw",
|
||||
"Broken Sword",
|
||||
"Prescription",
|
||||
"Eyeball Frog",
|
||||
"Eyedrops",
|
||||
"Claim Check",
|
||||
}
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
if not value:
|
||||
value = self.default
|
||||
super().__init__(value)
|
||||
class AdultTradeStart(Choice):
|
||||
"""Choose the item that starts the adult trade sequence."""
|
||||
display_name = "Adult Trade Sequence Start"
|
||||
option_pocket_egg = 0
|
||||
option_pocket_cucco = 1
|
||||
option_cojiro = 2
|
||||
option_odd_mushroom = 3
|
||||
option_poachers_saw = 4
|
||||
option_broken_sword = 5
|
||||
option_prescription = 6
|
||||
option_eyeball_frog = 7
|
||||
option_eyedrops = 8
|
||||
option_claim_check = 9
|
||||
default = 9
|
||||
|
||||
|
||||
itempool_options: typing.Dict[str, type(Option)] = {
|
||||
@@ -1068,7 +1160,7 @@ class Targeting(Choice):
|
||||
|
||||
|
||||
class DisplayDpad(DefaultOnToggle):
|
||||
"""Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots)."""
|
||||
"""Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots, mask)."""
|
||||
display_name = "Display D-Pad HUD"
|
||||
|
||||
|
||||
@@ -1191,7 +1283,6 @@ oot_options: typing.Dict[str, type(Option)] = {
|
||||
**world_options,
|
||||
**bridge_options,
|
||||
**dungeon_items_options,
|
||||
# **lacs_options,
|
||||
**shuffle_options,
|
||||
**timesavers_options,
|
||||
**misc_options,
|
||||
|
||||
@@ -2094,10 +2094,14 @@ def patch_rom(world, rom):
|
||||
if not world.dungeon_mq['Ganons Castle']:
|
||||
chest_name = 'Ganons Castle Light Trial Lullaby Chest'
|
||||
location = world.get_location(chest_name)
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if not location.item.trap:
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
looks_like_index = get_override_entry(world, location)[5]
|
||||
item = read_rom_item(rom, looks_like_index)
|
||||
if item['chest_type'] in (GOLD_CHEST, GILDED_CHEST, SKULL_CHEST_BIG):
|
||||
rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
|
||||
|
||||
@@ -2106,10 +2110,14 @@ def patch_rom(world, rom):
|
||||
chest_name = 'Spirit Temple Compass Chest'
|
||||
chest_address = 0x2B6B07C
|
||||
location = world.get_location(chest_name)
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if not location.item.trap:
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
looks_like_index = get_override_entry(world, location)[5]
|
||||
item = read_rom_item(rom, looks_like_index)
|
||||
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
|
||||
rom.write_int16(chest_address + 2, 0x0190) # X pos
|
||||
rom.write_int16(chest_address + 6, 0xFABC) # Z pos
|
||||
@@ -2120,10 +2128,14 @@ def patch_rom(world, rom):
|
||||
chest_address_0 = 0x21A02D0 # Address in setup 0
|
||||
chest_address_2 = 0x21A06E4 # Address in setup 2
|
||||
location = world.get_location(chest_name)
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
if not location.item.trap:
|
||||
if location.item.game == 'Ocarina of Time':
|
||||
item = read_rom_item(rom, location.item.index)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
else:
|
||||
item = read_rom_item(rom, AP_PROGRESSION if location.item.advancement else AP_JUNK)
|
||||
looks_like_index = get_override_entry(world, location)[5]
|
||||
item = read_rom_item(rom, looks_like_index)
|
||||
if item['chest_type'] in (BROWN_CHEST, SILVER_CHEST, SKULL_CHEST_SMALL):
|
||||
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
|
||||
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
|
||||
|
||||
@@ -223,9 +223,6 @@ def set_shop_rules(ootworld):
|
||||
# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
|
||||
def set_entrances_based_rules(ootworld):
|
||||
|
||||
if ootworld.multiworld.accessibility[ootworld.player] == 'minimal':
|
||||
return
|
||||
|
||||
all_state = ootworld.multiworld.get_all_state(False)
|
||||
|
||||
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
|
||||
|
||||
@@ -11,7 +11,7 @@ def data_path(*args):
|
||||
return os.path.join(os.path.dirname(__file__), 'data', *args)
|
||||
|
||||
|
||||
@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
|
||||
@lru_cache
|
||||
def read_json(file_path):
|
||||
json_string = ""
|
||||
with io.open(file_path, 'r') as file:
|
||||
|
||||
@@ -10,7 +10,7 @@ from string import printable
|
||||
|
||||
logger = logging.getLogger("Ocarina of Time")
|
||||
|
||||
from .Location import OOTLocation, LocationFactory, location_name_to_id
|
||||
from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
|
||||
from .Entrance import OOTEntrance
|
||||
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
|
||||
from .HintList import getRequiredHints
|
||||
@@ -163,11 +163,13 @@ class OOTWorld(World):
|
||||
"Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
|
||||
"Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
|
||||
"Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
|
||||
"Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Odd Mushroom",
|
||||
"Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
|
||||
"Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
|
||||
"Eyeball Frog", "Eyedrops", "Claim Check"}
|
||||
"Eyeball Frog", "Eyedrops", "Claim Check"},
|
||||
}
|
||||
|
||||
location_name_groups = build_location_name_groups()
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.hint_data_available = threading.Event()
|
||||
self.collectible_flags_available = threading.Event()
|
||||
@@ -384,6 +386,7 @@ class OOTWorld(World):
|
||||
self.mq_dungeons_mode = 'count'
|
||||
self.mq_dungeons_count = 0
|
||||
self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
|
||||
self.dungeon_mq['Thieves Hideout'] = False # fix for bug in SaveContext:287
|
||||
|
||||
# Empty dungeon placeholder for the moment
|
||||
self.empty_dungeons = {name: False for name in self.dungeon_mq}
|
||||
@@ -409,6 +412,9 @@ class OOTWorld(World):
|
||||
self.starting_tod = self.starting_tod.replace('_', '-')
|
||||
self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
|
||||
|
||||
# Convert adult trade option to expected Set
|
||||
self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
|
||||
|
||||
# Get hint distribution
|
||||
self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
|
||||
|
||||
@@ -446,7 +452,7 @@ class OOTWorld(World):
|
||||
self.always_hints = [hint.name for hint in getRequiredHints(self)]
|
||||
|
||||
# Determine items which are not considered advancement based on settings. They will never be excluded.
|
||||
self.nonadvancement_items = {'Double Defense'}
|
||||
self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
|
||||
if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
|
||||
self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
|
||||
# nayru's love may be required to prevent forced damage
|
||||
@@ -633,16 +639,18 @@ class OOTWorld(World):
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.hinted_dungeon_reward_locations[item.name] = loc
|
||||
|
||||
def create_item(self, name: str):
|
||||
def create_item(self, name: str, allow_arbitrary_name: bool = False):
|
||||
if name in item_table:
|
||||
return OOTItem(name, self.player, item_table[name], False,
|
||||
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
|
||||
None) else False))
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
if allow_arbitrary_name:
|
||||
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
|
||||
raise Exception(f"Invalid item name: {name}")
|
||||
|
||||
def make_event_item(self, name, location, item=None):
|
||||
if item is None:
|
||||
item = self.create_item(name)
|
||||
item = self.create_item(name, allow_arbitrary_name=True)
|
||||
self.multiworld.push_item(location, item, collect=False)
|
||||
location.locked = True
|
||||
location.event = True
|
||||
@@ -800,23 +808,25 @@ class OOTWorld(World):
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items,
|
||||
single_player_placement=True, lock=True)
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
else:
|
||||
for dungeon_info in dungeon_table:
|
||||
dungeon_name = dungeon_info['name']
|
||||
locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
|
||||
if isinstance(locations, list):
|
||||
dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
|
||||
if not dungeon_items:
|
||||
continue
|
||||
for item in dungeon_items:
|
||||
self.multiworld.itempool.remove(item)
|
||||
self.multiworld.random.shuffle(locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items,
|
||||
single_player_placement=True, lock=True)
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
if self.shuffle_song_items != 'any':
|
||||
tries = 5
|
||||
tries = 10
|
||||
if self.shuffle_song_items == 'song':
|
||||
song_locations = list(filter(lambda location: location.type == 'Song',
|
||||
self.multiworld.get_unfilled_locations(player=self.player)))
|
||||
@@ -852,7 +862,7 @@ class OOTWorld(World):
|
||||
try:
|
||||
self.multiworld.random.shuffle(song_locations)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:],
|
||||
True, True)
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
|
||||
except FillError as e:
|
||||
tries -= 1
|
||||
@@ -888,7 +898,8 @@ class OOTWorld(World):
|
||||
self.multiworld.random.shuffle(shop_locations)
|
||||
for item in shop_prog + shop_junk:
|
||||
self.multiworld.itempool.remove(item)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, True, True)
|
||||
fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog,
|
||||
single_player_placement=True, lock=True, allow_excluded=True)
|
||||
fast_fill(self.multiworld, shop_junk, shop_locations)
|
||||
for loc in shop_locations:
|
||||
loc.locked = True
|
||||
@@ -963,7 +974,7 @@ class OOTWorld(World):
|
||||
multiworld.itempool.remove(item)
|
||||
multiworld.random.shuffle(locations)
|
||||
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items,
|
||||
single_player_placement=False, lock=True)
|
||||
single_player_placement=False, lock=True, allow_excluded=True)
|
||||
if fill_stage == 'Song':
|
||||
# We don't want song locations to contain progression unless it's a song
|
||||
# or it was marked as priority.
|
||||
@@ -984,7 +995,7 @@ class OOTWorld(World):
|
||||
multiworld.itempool.remove(item)
|
||||
multiworld.random.shuffle(locations)
|
||||
fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items,
|
||||
single_player_placement=False, lock=True)
|
||||
single_player_placement=False, lock=True, allow_excluded=True)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
@@ -1051,7 +1062,10 @@ class OOTWorld(World):
|
||||
def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
|
||||
def hint_type_players(hint_type: str) -> set:
|
||||
return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
|
||||
if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0}
|
||||
if autoworld.hints != 'none'
|
||||
and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
|
||||
and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0
|
||||
or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
|
||||
|
||||
try:
|
||||
item_hint_players = hint_type_players('item')
|
||||
@@ -1078,10 +1092,10 @@ class OOTWorld(World):
|
||||
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
||||
(oot_is_item_of_type(loc.item, 'Song') or
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
|
||||
if loc.player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
||||
@@ -1096,7 +1110,12 @@ class OOTWorld(World):
|
||||
elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth
|
||||
for player in (barren_hint_players | woth_hint_players):
|
||||
for loc in multiworld.worlds[player].get_locations():
|
||||
if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')):
|
||||
if loc.item.code and (not loc.locked or
|
||||
(oot_is_item_of_type(loc.item, 'Song') or
|
||||
(oot_is_item_of_type(loc.item, 'SmallKey') and multiworld.worlds[loc.player].shuffle_smallkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'HideoutSmallKey') and multiworld.worlds[loc.player].shuffle_hideoutkeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'BossKey') and multiworld.worlds[loc.player].shuffle_bosskeys in ('overworld', 'any_dungeon', 'regional')) or
|
||||
(oot_is_item_of_type(loc.item, 'GanonBossKey') and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
|
||||
if player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[player][hint_area]['weight'] += 1
|
||||
@@ -1183,6 +1202,15 @@ class OOTWorld(World):
|
||||
er_hint_data[self.player][location.address] = main_entrance.name
|
||||
logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
|
||||
spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
|
||||
|
||||
if self.shopsanity != 'off':
|
||||
spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
|
||||
for k, v in self.shop_prices.items():
|
||||
spoiler_handle.write(f"{k}: {v} Rupees\n")
|
||||
|
||||
# Key ring handling:
|
||||
# Key rings are multiple items glued together into one, so we need to give
|
||||
# the appropriate number of keys in the collection state when they are
|
||||
@@ -1265,25 +1293,13 @@ class OOTWorld(World):
|
||||
# Specifically ensures that only real items are gotten, not any events.
|
||||
# In particular, ensures that Time Travel needs to be found.
|
||||
def get_state_with_complete_itempool(self):
|
||||
all_state = self.multiworld.get_all_state(use_cache=False)
|
||||
# Remove event progression items
|
||||
for item, player in all_state.prog_items:
|
||||
if player == self.player and (item not in item_table or item_table[item][2] is None):
|
||||
all_state.prog_items[(item, player)] = 0
|
||||
# Remove all events and checked locations
|
||||
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
|
||||
all_state.events = {loc for loc in all_state.events if loc.player != self.player}
|
||||
all_state = CollectionState(self.multiworld)
|
||||
for item in self.multiworld.itempool:
|
||||
if item.player == self.player:
|
||||
self.multiworld.worlds[item.player].collect(all_state, item)
|
||||
# If free_scarecrow give Scarecrow Song
|
||||
if self.free_scarecrow:
|
||||
all_state.collect(self.create_item("Scarecrow Song"), event=True)
|
||||
|
||||
# Invalidate caches
|
||||
all_state.child_reachable_regions[self.player] = set()
|
||||
all_state.adult_reachable_regions[self.player] = set()
|
||||
all_state.child_blocked_connections[self.player] = set()
|
||||
all_state.adult_blocked_connections[self.player] = set()
|
||||
all_state.day_reachable_regions[self.player] = set()
|
||||
all_state.dampe_reachable_regions[self.player] = set()
|
||||
all_state.stale[self.player] = True
|
||||
|
||||
return all_state
|
||||
@@ -1349,7 +1365,7 @@ def gather_locations(multiworld: MultiWorld,
|
||||
condition = lambda location: location.name in dungeon_song_locations
|
||||
locations += filter(condition, multiworld.get_unfilled_locations(player=player))
|
||||
else:
|
||||
if any(map(lambda v: v in {'keysanity'}, fill_opts.values())):
|
||||
if any(map(lambda v: v == 'keysanity', fill_opts.values())):
|
||||
return None
|
||||
for player, option in fill_opts.items():
|
||||
condition = functools.partial(valid_dungeon_item_location,
|
||||
|
||||
@@ -172,7 +172,7 @@ class Overcooked2World(World):
|
||||
# random priority locations have no desirable effect on solo seeds
|
||||
return list()
|
||||
|
||||
balancing_mode = self.get_options()["LocationBalancing"]
|
||||
balancing_mode = self.options.location_balancing
|
||||
|
||||
if balancing_mode == LocationBalancingMode.disabled:
|
||||
# Location balancing is disabled, progression density is purely determined by filler
|
||||
@@ -528,7 +528,7 @@ class Overcooked2World(World):
|
||||
# Game Modifications
|
||||
"LevelPurchaseRequirements": level_purchase_requirements,
|
||||
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
|
||||
"ShortHordeLevels": self.options.short_horde_levels,
|
||||
"ShortHordeLevels": self.options.short_horde_levels.result,
|
||||
"CustomLevelOrder": custom_level_order,
|
||||
|
||||
# Items (Starting Inventory)
|
||||
@@ -584,6 +584,7 @@ class Overcooked2World(World):
|
||||
"TwoStars": star_threshold_scale * 0.75,
|
||||
"OneStar": star_threshold_scale * 0.35,
|
||||
}
|
||||
base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result
|
||||
|
||||
return base_data
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
## Required Software
|
||||
|
||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Version 2.3.1 and later are supported. Version 2.9.1 is recommended.
|
||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
@@ -23,7 +23,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
|
||||
|
||||
Once BizHawk has been installed, open EmuHawk and change the following settings:
|
||||
|
||||
- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
- (If using 2.8 or earlier) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
"Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly.
|
||||
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
|
||||
**of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
|
||||
@@ -57,7 +57,7 @@ For `trainer_name` and `rival_name` the following regular characters are allowed
|
||||
|
||||
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
|
||||
|
||||
And the following special characters (these each take up one character):
|
||||
And the following special characters (these each count as one character):
|
||||
* `<'d>`
|
||||
* `<'l>`
|
||||
* `<'t>`
|
||||
|
||||
@@ -7,7 +7,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
|
||||
## Software Requerido
|
||||
|
||||
- BizHawk: [BizHawk Releases en TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.7 para estabilidad.
|
||||
- La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.9.1.
|
||||
- Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba.
|
||||
- Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se
|
||||
encuentra en el enlace de arriba.
|
||||
|
||||
@@ -795,7 +795,7 @@ location_data = [
|
||||
LocationData("Pewter Gym", "Defeat Brock", "Defeat Brock", event=True),
|
||||
LocationData("Cerulean Gym", "Defeat Misty", "Defeat Misty", event=True),
|
||||
LocationData("Vermilion Gym", "Defeat Lt. Surge", "Defeat Lt. Surge", event=True),
|
||||
LocationData("Celadon Gym", "Defeat Erika", "Defeat Erika", event=True),
|
||||
LocationData("Celadon Gym-C", "Defeat Erika", "Defeat Erika", event=True),
|
||||
LocationData("Fuchsia Gym", "Defeat Koga", "Defeat Koga", event=True),
|
||||
LocationData("Cinnabar Gym", "Defeat Blaine", "Defeat Blaine", event=True),
|
||||
LocationData("Saffron Gym-C", "Defeat Sabrina", "Defeat Sabrina", event=True),
|
||||
|
||||
@@ -1456,7 +1456,9 @@ mansion_stair_destinations = [
|
||||
unreachable_outdoor_entrances = [
|
||||
"Route 4-C to Mt Moon B1F-NE",
|
||||
"Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House",
|
||||
"Cerulean City-Badge House Backyard to Cerulean Badge House"
|
||||
"Cerulean City-Badge House Backyard to Cerulean Badge House",
|
||||
# TODO: This doesn't need to be forced if fly location is Pokemon League?
|
||||
"Route 23-N to Victory Road 2F-E"
|
||||
]
|
||||
|
||||
|
||||
@@ -1592,7 +1594,7 @@ def create_regions(self):
|
||||
connect(multiworld, player, "Menu", "Pallet Town", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Pokedex", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Evolution", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
|
||||
connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
|
||||
state.multiworld.second_fossil_check_condition[player].value, player), one_way=True)
|
||||
connect(multiworld, player, "Pallet Town", "Route 1")
|
||||
connect(multiworld, player, "Route 1", "Viridian City")
|
||||
@@ -2220,7 +2222,7 @@ def create_regions(self):
|
||||
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]:
|
||||
badge_locs.append(multiworld.get_location(loc, player))
|
||||
multiworld.random.shuffle(badges)
|
||||
while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player] == "on":
|
||||
while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]:
|
||||
multiworld.random.shuffle(badges)
|
||||
for badge, loc in zip(badges, badge_locs):
|
||||
loc.place_locked_item(badge)
|
||||
@@ -2265,22 +2267,30 @@ def create_regions(self):
|
||||
"Defeat Viridian Gym Giovanni",
|
||||
]
|
||||
|
||||
def adds_reachable_entrances(entrances_copy, item):
|
||||
state.collect(item, False)
|
||||
event_locations = self.multiworld.get_filled_locations(player)
|
||||
|
||||
def adds_reachable_entrances(entrances_copy, item, dead_end_cache):
|
||||
ret = dead_end_cache.get(item.name)
|
||||
if (ret != None):
|
||||
return ret
|
||||
|
||||
state_copy = state.copy()
|
||||
state_copy.collect(item, True)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or
|
||||
entrance.parent_region.can_reach(state)]) > len(reachable_entrances)
|
||||
state.remove(item)
|
||||
entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances)
|
||||
dead_end_cache[item.name] = ret
|
||||
return ret
|
||||
|
||||
def dead_end(entrances_copy, e):
|
||||
def dead_end(entrances_copy, e, dead_end_cache):
|
||||
region = e.parent_region
|
||||
check_warps = set()
|
||||
checked_regions = {region}
|
||||
check_warps.update(region.exits)
|
||||
check_warps.remove(e)
|
||||
for location in region.locations:
|
||||
if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy,
|
||||
location.item):
|
||||
if location.item and location.item.name in relevant_events and \
|
||||
adds_reachable_entrances(entrances_copy, location.item, dead_end_cache):
|
||||
return False
|
||||
while check_warps:
|
||||
warp = check_warps.pop()
|
||||
@@ -2297,16 +2307,22 @@ def create_regions(self):
|
||||
check_warps.update(warp.connected_region.exits)
|
||||
for location in warp.connected_region.locations:
|
||||
if (location.item and location.item.name in relevant_events and
|
||||
adds_reachable_entrances(entrances_copy, location.item)):
|
||||
adds_reachable_entrances(entrances_copy, location.item, dead_end_cache)):
|
||||
return False
|
||||
return True
|
||||
|
||||
starting_entrances = len(entrances)
|
||||
dc_connected = []
|
||||
event_locations = self.multiworld.get_filled_locations(player)
|
||||
rock_tunnel_entrances = [entrance for entrance in entrances if "Rock Tunnel" in entrance.name]
|
||||
entrances = [entrance for entrance in entrances if entrance not in rock_tunnel_entrances]
|
||||
while entrances:
|
||||
state.update_reachable_regions(player)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
|
||||
if rock_tunnel_entrances and logic.rock_tunnel(state, player):
|
||||
entrances += rock_tunnel_entrances
|
||||
rock_tunnel_entrances = None
|
||||
|
||||
reachable_entrances = [entrance for entrance in entrances if entrance in reachable_entrances or
|
||||
entrance.parent_region.can_reach(state)]
|
||||
assert reachable_entrances, \
|
||||
@@ -2321,30 +2337,29 @@ def create_regions(self):
|
||||
if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances):
|
||||
entrances.sort(key=lambda e: e.name not in entrance_only)
|
||||
|
||||
if len(entrances) < 48 and multiworld.door_shuffle[player] == "full":
|
||||
# Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached
|
||||
# except by connecting directly to it.
|
||||
entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances)
|
||||
dead_end_cache = {}
|
||||
|
||||
# entrances list is empty while it's being sorted, must pass a copy to iterate through
|
||||
entrances_copy = entrances.copy()
|
||||
if multiworld.door_shuffle[player] == "decoupled":
|
||||
if len(reachable_entrances) <= 8 and not logic.rock_tunnel(state, player):
|
||||
entrances.sort(key=lambda e: 1 if "Rock Tunnel" in e.name else 2 if e.connected_region is not
|
||||
None else 3 if e not in reachable_entrances else 0)
|
||||
else:
|
||||
entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in
|
||||
reachable_entrances else 0)
|
||||
entrances.sort(key=lambda e: 1 if e.connected_region is not None else 2 if e not in
|
||||
reachable_entrances else 0)
|
||||
assert entrances[0].connected_region is None,\
|
||||
"Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
|
||||
elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len(
|
||||
entrances) <= (starting_entrances - 3):
|
||||
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if
|
||||
dead_end(entrances_copy, e) else 1)
|
||||
dead_end(entrances_copy, e, dead_end_cache) else 1)
|
||||
else:
|
||||
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if
|
||||
dead_end(entrances_copy, e) else 2)
|
||||
dead_end(entrances_copy, e, dead_end_cache) else 2)
|
||||
if multiworld.door_shuffle[player] == "full":
|
||||
outdoor = outdoor_map(entrances[0].parent_region.name)
|
||||
if len(entrances) < 48 and not outdoor:
|
||||
# Prevent a situation where the only remaining outdoor entrances are ones that cannot be reached
|
||||
# except by connecting directly to it.
|
||||
entrances.sort(key=lambda e: e.name in unreachable_outdoor_entrances)
|
||||
|
||||
entrances.sort(key=lambda e: outdoor_map(e.parent_region.name) != outdoor)
|
||||
assert entrances[0] in reachable_entrances, \
|
||||
"Ran out of valid reachable entrances in Pokemon Red and Blue door shuffle"
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import copy
|
||||
import os
|
||||
import threading
|
||||
import base64
|
||||
import settings
|
||||
import copy
|
||||
import logging
|
||||
import threading
|
||||
import typing
|
||||
from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
|
||||
from worlds.generic.Rules import set_rule, add_rule, add_item_rule
|
||||
import settings
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import AutoLogicRegister, WebWorld, World
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
|
||||
logger = logging.getLogger("Super Metroid")
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ them. Player settings page: [Super Mario World Player Settings Page](/games/Supe
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-setti
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Protocol, Set
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
from . import pyevermizer
|
||||
from .Options import EnergyCore
|
||||
from .Options import EnergyCore, OutOfBounds, SequenceBreaks
|
||||
|
||||
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
|
||||
|
||||
@@ -61,4 +61,10 @@ class SecretOfEvermoreLogic(LogicMixin):
|
||||
if w.energy_core == EnergyCore.option_fragments:
|
||||
progress = pyevermizer.P_CORE_FRAGMENT
|
||||
count = w.required_fragments
|
||||
elif progress == pyevermizer.P_ALLOW_OOB:
|
||||
if world.worlds[player].out_of_bounds == OutOfBounds.option_logic:
|
||||
return True
|
||||
elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
|
||||
if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic:
|
||||
return True
|
||||
return self._soe_count(progress, world, player, count) >= count
|
||||
|
||||
@@ -38,6 +38,12 @@ class OffOnFullChoice(Choice):
|
||||
alias_chaos = 2
|
||||
|
||||
|
||||
class OffOnLogicChoice(Choice):
|
||||
option_off = 0
|
||||
option_on = 1
|
||||
option_logic = 2
|
||||
|
||||
|
||||
# actual options
|
||||
class Difficulty(EvermizerFlags, Choice):
|
||||
"""Changes relative spell cost and stuff"""
|
||||
@@ -93,10 +99,18 @@ class ExpModifier(Range):
|
||||
default = 200
|
||||
|
||||
|
||||
class FixSequence(EvermizerFlag, DefaultOnToggle):
|
||||
"""Fix some sequence breaks"""
|
||||
display_name = "Fix Sequence"
|
||||
flag = '1'
|
||||
class SequenceBreaks(EvermizerFlags, OffOnLogicChoice):
|
||||
"""Disable, enable some sequence breaks or put them in logic"""
|
||||
display_name = "Sequence Breaks"
|
||||
default = 0
|
||||
flags = ['', 'j', 'J']
|
||||
|
||||
|
||||
class OutOfBounds(EvermizerFlags, OffOnLogicChoice):
|
||||
"""Disable, enable the out-of-bounds glitch or put it in logic"""
|
||||
display_name = "Out Of Bounds"
|
||||
default = 0
|
||||
flags = ['', 'u', 'U']
|
||||
|
||||
|
||||
class FixCheats(EvermizerFlag, DefaultOnToggle):
|
||||
@@ -240,7 +254,8 @@ soe_options: typing.Dict[str, AssembleOptions] = {
|
||||
"available_fragments": AvailableFragments,
|
||||
"money_modifier": MoneyModifier,
|
||||
"exp_modifier": ExpModifier,
|
||||
"fix_sequence": FixSequence,
|
||||
"sequence_breaks": SequenceBreaks,
|
||||
"out_of_bounds": OutOfBounds,
|
||||
"fix_cheats": FixCheats,
|
||||
"fix_infinite_ammo": FixInfiniteAmmo,
|
||||
"fix_atlas_glitch": FixAtlasGlitch,
|
||||
|
||||
@@ -10,12 +10,8 @@ from worlds.generic.Rules import add_item_rule, set_rule
|
||||
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
|
||||
from Utils import output_path
|
||||
|
||||
try:
|
||||
import pyevermizer # from package
|
||||
except ImportError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
from . import pyevermizer # as part of the source tree
|
||||
import pyevermizer # from package
|
||||
# from . import pyevermizer # as part of the source tree
|
||||
|
||||
from . import Logic # load logic mixin
|
||||
from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
|
||||
@@ -179,6 +175,8 @@ class SoEWorld(World):
|
||||
evermizer_seed: int
|
||||
connect_name: str
|
||||
energy_core: int
|
||||
sequence_breaks: int
|
||||
out_of_bounds: int
|
||||
available_fragments: int
|
||||
required_fragments: int
|
||||
|
||||
@@ -191,6 +189,8 @@ class SoEWorld(World):
|
||||
def generate_early(self) -> None:
|
||||
# store option values that change logic
|
||||
self.energy_core = self.multiworld.energy_core[self.player].value
|
||||
self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value
|
||||
self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value
|
||||
self.required_fragments = self.multiworld.required_fragments[self.player].value
|
||||
if self.required_fragments > self.multiworld.available_fragments[self.player].value:
|
||||
self.multiworld.available_fragments[self.player].value = self.required_fragments
|
||||
@@ -224,9 +224,8 @@ class SoEWorld(World):
|
||||
max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
|
||||
|
||||
# TODO: generate *some* regions from locations' requirements?
|
||||
r = Region('Menu', self.player, self.multiworld)
|
||||
r.exits = [Entrance(self.player, 'New Game', r)]
|
||||
self.multiworld.regions += [r]
|
||||
menu = Region('Menu', self.player, self.multiworld)
|
||||
self.multiworld.regions += [menu]
|
||||
|
||||
def get_sphere_index(evermizer_loc):
|
||||
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
|
||||
@@ -234,11 +233,14 @@ class SoEWorld(World):
|
||||
return 2
|
||||
return min(2, len(evermizer_loc.requires))
|
||||
|
||||
# create ingame region
|
||||
ingame = Region('Ingame', self.player, self.multiworld)
|
||||
|
||||
# group locations into spheres (1, 2, 3+ at index 0, 1, 2)
|
||||
spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {}
|
||||
for loc in _locations:
|
||||
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
|
||||
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r,
|
||||
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
|
||||
loc.difficulty > max_difficulty))
|
||||
|
||||
# location balancing data
|
||||
@@ -280,18 +282,16 @@ class SoEWorld(World):
|
||||
late_locations = self.multiworld.random.sample(late_bosses, late_count)
|
||||
|
||||
# add locations to the world
|
||||
r = Region('Ingame', self.player, self.multiworld)
|
||||
for sphere in spheres.values():
|
||||
for locations in sphere.values():
|
||||
for location in locations:
|
||||
r.locations.append(location)
|
||||
ingame.locations.append(location)
|
||||
if location.name in late_locations:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
r.locations.append(SoELocation(self.player, 'Done', None, r))
|
||||
self.multiworld.regions += [r]
|
||||
|
||||
self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player))
|
||||
ingame.locations.append(SoELocation(self.player, 'Done', None, ingame))
|
||||
menu.connect(ingame, "New Game")
|
||||
self.multiworld.regions += [ingame]
|
||||
|
||||
def create_items(self):
|
||||
# add regular items to the pool
|
||||
|
||||
@@ -29,7 +29,7 @@ them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Sec
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator
|
||||
page: [YAML Validation page](/mysterycheck)
|
||||
page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8'
|
||||
#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11'
|
||||
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64')
|
||||
pyevermizer==0.46.1 \
|
||||
--hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \
|
||||
--hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \
|
||||
--hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \
|
||||
--hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \
|
||||
--hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \
|
||||
--hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \
|
||||
--hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \
|
||||
--hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \
|
||||
--hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \
|
||||
--hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \
|
||||
--hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \
|
||||
--hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \
|
||||
--hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \
|
||||
--hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \
|
||||
--hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \
|
||||
--hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \
|
||||
--hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \
|
||||
--hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \
|
||||
--hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \
|
||||
--hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \
|
||||
--hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \
|
||||
--hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \
|
||||
--hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \
|
||||
--hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \
|
||||
--hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \
|
||||
--hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \
|
||||
--hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \
|
||||
--hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \
|
||||
--hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \
|
||||
--hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \
|
||||
--hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \
|
||||
--hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \
|
||||
--hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \
|
||||
--hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \
|
||||
--hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
class SoETestBase(WorldTestBase):
|
||||
game = "Secret of Evermore"
|
||||
|
||||
def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
|
||||
satisfied=True) -> None:
|
||||
"""
|
||||
Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
|
||||
Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
|
||||
"""
|
||||
for location in reachable:
|
||||
self.assertEqual(self.can_reach_location(location), satisfied,
|
||||
f"{location} is unreachable but should be" if satisfied else
|
||||
f"{location} is reachable but shouldn't be")
|
||||
for location in unreachable:
|
||||
self.assertFalse(self.can_reach_location(location),
|
||||
f"{location} is reachable but shouldn't be")
|
||||
|
||||
51
worlds/soe/test/test_oob.py
Normal file
51
worlds/soe/test/test_oob.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import typing
|
||||
from . import SoETestBase
|
||||
|
||||
|
||||
class OoBTest(SoETestBase):
|
||||
"""Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
|
||||
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
|
||||
|
||||
def testOoBAccess(self):
|
||||
in_logic = self.options["out_of_bounds"] == "logic"
|
||||
|
||||
# some locations that just need a weapon + OoB
|
||||
oob_reachable = [
|
||||
"Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip
|
||||
"Levitate", "Fireball", "Drain", "Speed",
|
||||
"E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
|
||||
]
|
||||
# some locations that should still be unreachable
|
||||
oob_unreachable = [
|
||||
"Tiny", "Rimsala",
|
||||
"Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances
|
||||
"Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275",
|
||||
]
|
||||
# OoB + Diamond Eyes
|
||||
de_reachable = [
|
||||
"Tiny's hideout #160",
|
||||
]
|
||||
# still unreachable
|
||||
de_unreachable = [
|
||||
"Tiny",
|
||||
"Tiny's hideout #161",
|
||||
]
|
||||
|
||||
self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False)
|
||||
self.collect_by_name("Gladiator Sword")
|
||||
self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic)
|
||||
self.collect_by_name("Diamond Eye")
|
||||
self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
|
||||
|
||||
def testOoBGoal(self):
|
||||
# still need Energy Core with OoB if sequence breaks are not in logic
|
||||
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
|
||||
self.collect_by_name(item)
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name("Energy Core")
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class OoBInLogicTest(OoBTest):
|
||||
"""Tests that stuff that should be reachable/unreachable with out-of-bounds actually is."""
|
||||
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"}
|
||||
45
worlds/soe/test/test_sequence_breaks.py
Normal file
45
worlds/soe/test/test_sequence_breaks.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import typing
|
||||
from . import SoETestBase
|
||||
|
||||
|
||||
class SequenceBreaksTest(SoETestBase):
|
||||
"""Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
|
||||
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
|
||||
|
||||
def testSequenceBreaksAccess(self):
|
||||
in_logic = self.options["sequence_breaks"] == "logic"
|
||||
|
||||
# some locations that just need any weapon + sequence break
|
||||
break_reachable = [
|
||||
"Sons of Sth.", "Mad Monk", "Magmar",
|
||||
"Fireball",
|
||||
"Volcano Room1 #73", "Pyramid top #135",
|
||||
]
|
||||
# some locations that should still be unreachable
|
||||
break_unreachable = [
|
||||
"Aquagoth", "Megataur", "Tiny", "Rimsala",
|
||||
"Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape",
|
||||
"Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
|
||||
]
|
||||
|
||||
self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False)
|
||||
self.collect_by_name("Gladiator Sword")
|
||||
self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic)
|
||||
self.collect_by_name("Spider Claw") # Gauge now just needs non-sword
|
||||
self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic)
|
||||
self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
|
||||
self.assertEqual(self.can_reach_location("Escape"), in_logic)
|
||||
|
||||
def testSequenceBreaksGoal(self):
|
||||
in_logic = self.options["sequence_breaks"] == "logic"
|
||||
|
||||
# don't need Energy Core with sequence breaks in logic
|
||||
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(item)
|
||||
self.assertBeatable(in_logic)
|
||||
|
||||
|
||||
class SequenceBreaksInLogicTest(SequenceBreaksTest):
|
||||
"""Tests that stuff that should be reachable/unreachable with sequence breaks actually is."""
|
||||
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"}
|
||||
@@ -100,15 +100,15 @@ class StardewValleyWorld(World):
|
||||
return region
|
||||
|
||||
world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options)
|
||||
self.multiworld.regions.extend(world_regions)
|
||||
|
||||
def add_location(name: str, code: Optional[int], region: str):
|
||||
region = self.multiworld.get_region(region, self.player)
|
||||
region = world_regions[region]
|
||||
location = StardewLocation(self.player, name, code, region)
|
||||
location.access_rule = lambda _: True
|
||||
region.locations.append(location)
|
||||
|
||||
create_locations(add_location, self.options, self.multiworld.random)
|
||||
self.multiworld.regions.extend(world_regions.values())
|
||||
|
||||
def create_items(self):
|
||||
self.precollect_starting_season()
|
||||
|
||||
@@ -368,8 +368,8 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard
|
||||
|
||||
|
||||
def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
|
||||
items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value)
|
||||
items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value)
|
||||
items.extend(item_factory(item) for item in [Buff.movement] * options.movement_buff_number.value)
|
||||
items.extend(item_factory(item) for item in [Buff.luck] * options.luck_buff_number.value)
|
||||
|
||||
|
||||
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
|
||||
|
||||
@@ -927,7 +927,7 @@ class StardewLogic:
|
||||
return region_rule & ((tool_rule & foraging_rule) | magic_rule)
|
||||
|
||||
def has_max_buffs(self) -> StardewRule:
|
||||
return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value)
|
||||
return self.received(Buff.movement, self.options.movement_buff_number.value) & self.received(Buff.luck, self.options.luck_buff_number.value)
|
||||
|
||||
def get_weapon_rule_for_floor_tier(self, tier: int):
|
||||
if tier >= 4:
|
||||
@@ -1376,7 +1376,7 @@ class StardewLogic:
|
||||
return self.received(Wallet.rusty_key)
|
||||
|
||||
def can_win_egg_hunt(self) -> StardewRule:
|
||||
number_of_movement_buffs = self.options.number_of_movement_buffs.value
|
||||
number_of_movement_buffs = self.options.movement_buff_number.value
|
||||
if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
|
||||
return True_()
|
||||
return self.received(Buff.movement, number_of_movement_buffs // 2)
|
||||
|
||||
@@ -556,8 +556,8 @@ class StardewValleyOptions(PerGameCommonOptions):
|
||||
museumsanity: Museumsanity
|
||||
friendsanity: Friendsanity
|
||||
friendsanity_heart_size: FriendsanityHeartSize
|
||||
number_of_movement_buffs: NumberOfMovementBuffs
|
||||
number_of_luck_buffs: NumberOfLuckBuffs
|
||||
movement_buff_number: NumberOfMovementBuffs
|
||||
luck_buff_number: NumberOfLuckBuffs
|
||||
exclude_ginger_island: ExcludeGingerIsland
|
||||
trap_items: TrapItems
|
||||
multiple_day_sleep_enabled: MultipleDaySleepEnabled
|
||||
|
||||
@@ -429,7 +429,7 @@ def create_final_connections(world_options) -> List[ConnectionData]:
|
||||
|
||||
|
||||
def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[
|
||||
Iterable[Region], Dict[str, str]]:
|
||||
Dict[str, Region], Dict[str, str]]:
|
||||
final_regions = create_final_regions(world_options)
|
||||
regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in
|
||||
final_regions}
|
||||
@@ -444,7 +444,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options)
|
||||
if connection.name in entrances:
|
||||
entrances[connection.name].connect(regions[connection.destination])
|
||||
|
||||
return regions.values(), randomized_data
|
||||
return regions, randomized_data
|
||||
|
||||
|
||||
def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[
|
||||
|
||||
@@ -6,12 +6,12 @@ from typing import List, Dict, Any, cast
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from . import Items
|
||||
from . import Locations
|
||||
from . import Creatures
|
||||
from . import Options
|
||||
from .Items import item_table, group_items, items_by_type, ItemType
|
||||
from .Rules import set_rules
|
||||
from . import items
|
||||
from . import locations
|
||||
from . import creatures
|
||||
from . import options
|
||||
from .items import item_table, group_items, items_by_type, ItemType
|
||||
from .rules import set_rules
|
||||
|
||||
logger = logging.getLogger("Subnautica")
|
||||
|
||||
@@ -27,8 +27,8 @@ class SubnaticaWeb(WebWorld):
|
||||
)]
|
||||
|
||||
|
||||
all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()}
|
||||
all_locations.update(Creatures.creature_locations)
|
||||
all_locations = {data["name"]: loc_id for loc_id, data in locations.location_table.items()}
|
||||
all_locations.update(creatures.creature_locations)
|
||||
|
||||
|
||||
class SubnauticaWorld(World):
|
||||
@@ -40,9 +40,9 @@ class SubnauticaWorld(World):
|
||||
game = "Subnautica"
|
||||
web = SubnaticaWeb()
|
||||
|
||||
item_name_to_id = {data.name: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
option_definitions = Options.options
|
||||
option_definitions = options.option_definitions
|
||||
|
||||
data_version = 10
|
||||
required_client_version = (0, 4, 1)
|
||||
@@ -50,37 +50,53 @@ class SubnauticaWorld(World):
|
||||
creatures_to_scan: List[str]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.multiworld.early_seaglide[self.player]:
|
||||
if self.options.early_seaglide:
|
||||
self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2
|
||||
|
||||
scan_option: Options.AggressiveScanLogic = self.multiworld.creature_scan_logic[self.player]
|
||||
scan_option: options.AggressiveScanLogic = self.options.creature_scan_logic
|
||||
creature_pool = scan_option.get_pool()
|
||||
|
||||
self.multiworld.creature_scans[self.player].value = min(
|
||||
self.options.creature_scans.value = min(
|
||||
len(creature_pool),
|
||||
self.multiworld.creature_scans[self.player].value
|
||||
self.options.creature_scans.value
|
||||
)
|
||||
|
||||
self.creatures_to_scan = self.multiworld.random.sample(
|
||||
creature_pool, self.multiworld.creature_scans[self.player].value)
|
||||
self.creatures_to_scan = self.random.sample(
|
||||
creature_pool, self.options.creature_scans.value)
|
||||
|
||||
def create_regions(self):
|
||||
self.multiworld.regions += [
|
||||
self.create_region("Menu", None, ["Lifepod 5"]),
|
||||
self.create_region("Planet 4546B",
|
||||
Locations.events +
|
||||
[location["name"] for location in Locations.location_table.values()] +
|
||||
[creature+Creatures.suffix for creature in self.creatures_to_scan])
|
||||
]
|
||||
# Create Regions
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
||||
|
||||
# Link regions
|
||||
self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player))
|
||||
# Link regions together
|
||||
menu_region.connect(planet_region, "Lifepod 5")
|
||||
|
||||
for event in Locations.events:
|
||||
self.multiworld.get_location(event, self.player).place_locked_item(
|
||||
# Create regular locations
|
||||
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
||||
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
||||
for location_name in location_names:
|
||||
loc_id = self.location_name_to_id[location_name]
|
||||
location = SubnauticaLocation(self.player, location_name, loc_id, planet_region)
|
||||
planet_region.locations.append(location)
|
||||
|
||||
# Create events
|
||||
goal_event_name = self.options.goal.get_event_name()
|
||||
|
||||
for event in locations.events:
|
||||
location = SubnauticaLocation(self.player, event, None, planet_region)
|
||||
planet_region.locations.append(location)
|
||||
location.place_locked_item(
|
||||
SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
|
||||
# make the goal event the victory "item"
|
||||
self.multiworld.get_location(self.multiworld.goal[self.player].get_event_name(), self.player).item.name = "Victory"
|
||||
if event == goal_event_name:
|
||||
# make the goal event the victory "item"
|
||||
location.item.name = "Victory"
|
||||
|
||||
# Register regions to multiworld
|
||||
self.multiworld.regions += [
|
||||
menu_region,
|
||||
planet_region
|
||||
]
|
||||
|
||||
# refer to Rules.py
|
||||
set_rules = set_rules
|
||||
@@ -88,7 +104,7 @@ class SubnauticaWorld(World):
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
pool: List[SubnauticaItem] = []
|
||||
extras = self.multiworld.creature_scans[self.player].value
|
||||
extras = self.options.creature_scans.value
|
||||
|
||||
grouped = set(itertools.chain.from_iterable(group_items.values()))
|
||||
|
||||
@@ -139,17 +155,15 @@ class SubnauticaWorld(World):
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
goal: Options.Goal = self.multiworld.goal[self.player]
|
||||
swim_rule: Options.SwimRule = self.multiworld.swim_rule[self.player]
|
||||
vanilla_tech: List[str] = []
|
||||
|
||||
slot_data: Dict[str, Any] = {
|
||||
"goal": goal.current_key,
|
||||
"swim_rule": swim_rule.current_key,
|
||||
"goal": self.options.goal.current_key,
|
||||
"swim_rule": self.options.swim_rule.current_key,
|
||||
"vanilla_tech": vanilla_tech,
|
||||
"creatures_to_scan": self.creatures_to_scan,
|
||||
"death_link": self.multiworld.death_link[self.player].value,
|
||||
"free_samples": self.multiworld.free_samples[self.player].value,
|
||||
"death_link": self.options.death_link.value,
|
||||
"free_samples": self.options.free_samples.value,
|
||||
}
|
||||
|
||||
return slot_data
|
||||
@@ -161,10 +175,10 @@ class SubnauticaWorld(World):
|
||||
item_table[item_id].classification,
|
||||
item_id, player=self.player)
|
||||
|
||||
def create_region(self, name: str, locations=None, exits=None):
|
||||
def create_region(self, name: str, region_locations=None, exits=None):
|
||||
ret = Region(name, self.player, self.multiworld)
|
||||
if locations:
|
||||
for location in locations:
|
||||
if region_locations:
|
||||
for location in region_locations:
|
||||
loc_id = self.location_name_to_id.get(location, None)
|
||||
location = SubnauticaLocation(self.player, location, loc_id, ret)
|
||||
ret.locations.append(location)
|
||||
|
||||
@@ -12,8 +12,8 @@ if __name__ == "__main__":
|
||||
os.chdir(new_home)
|
||||
sys.path.append(new_home)
|
||||
|
||||
from worlds.subnautica.Locations import Vector, location_table
|
||||
from worlds.subnautica.Items import item_table, group_items, items_by_type
|
||||
from worlds.subnautica.locations import Vector, location_table
|
||||
from worlds.subnautica.items import item_table, group_items, items_by_type
|
||||
from NetUtils import encode
|
||||
|
||||
export_folder = os.path.join(new_home, "Subnautica Export")
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
|
||||
from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool
|
||||
from .Creatures import all_creatures, Definitions
|
||||
from .creatures import all_creatures, Definitions
|
||||
|
||||
|
||||
class SwimRule(Choice):
|
||||
@@ -103,7 +103,7 @@ class SubnauticaDeathLink(DeathLink):
|
||||
Note: can be toggled via in-game console command "deathlink"."""
|
||||
|
||||
|
||||
options = {
|
||||
option_definitions = {
|
||||
"swim_rule": SwimRule,
|
||||
"early_seaglide": EarlySeaglide,
|
||||
"free_samples": FreeSamples,
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import TYPE_CHECKING, Dict, Callable, Optional
|
||||
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Locations import location_table, LocationDict
|
||||
from .Creatures import all_creatures, aggressive, suffix, hatchable, containment
|
||||
from .Options import AggressiveScanLogic, SwimRule
|
||||
from .locations import location_table, LocationDict
|
||||
from .creatures import all_creatures, aggressive, suffix, hatchable, containment
|
||||
from .options import AggressiveScanLogic, SwimRule
|
||||
import math
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -290,16 +290,16 @@ aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = {
|
||||
|
||||
def set_rules(subnautica_world: "SubnauticaWorld"):
|
||||
player = subnautica_world.player
|
||||
world = subnautica_world.multiworld
|
||||
multiworld = subnautica_world.multiworld
|
||||
|
||||
for loc in location_table.values():
|
||||
set_location_rule(world, player, loc)
|
||||
set_location_rule(multiworld, player, loc)
|
||||
|
||||
if subnautica_world.creatures_to_scan:
|
||||
option = world.creature_scan_logic[player]
|
||||
option = multiworld.creature_scan_logic[player]
|
||||
|
||||
for creature_name in subnautica_world.creatures_to_scan:
|
||||
location = set_creature_rule(world, player, creature_name)
|
||||
location = set_creature_rule(multiworld, player, creature_name)
|
||||
if creature_name in containment: # there is no other way, hard-required containment
|
||||
add_rule(location, lambda state: has_containment(state, player))
|
||||
elif creature_name in aggressive:
|
||||
@@ -309,7 +309,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"):
|
||||
lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player))
|
||||
|
||||
# Victory locations
|
||||
set_rule(world.get_location("Neptune Launch", player),
|
||||
set_rule(multiworld.get_location("Neptune Launch", player),
|
||||
lambda state:
|
||||
get_max_depth(state, player) >= 1444 and
|
||||
has_mobile_vehicle_bay(state, player) and
|
||||
@@ -322,13 +322,14 @@ def set_rules(subnautica_world: "SubnauticaWorld"):
|
||||
state.has("Ion Battery", player) and
|
||||
has_cyclops_shield(state, player))
|
||||
|
||||
set_rule(world.get_location("Disable Quarantine", player), lambda state:
|
||||
get_max_depth(state, player) >= 1444)
|
||||
set_rule(multiworld.get_location("Disable Quarantine", player),
|
||||
lambda state: get_max_depth(state, player) >= 1444)
|
||||
|
||||
set_rule(world.get_location("Full Infection", player), lambda state:
|
||||
get_max_depth(state, player) >= 900)
|
||||
set_rule(multiworld.get_location("Full Infection", player),
|
||||
lambda state: get_max_depth(state, player) >= 900)
|
||||
|
||||
room = world.get_location("Aurora Drive Room - Upgrade Console", player)
|
||||
set_rule(world.get_location("Repair Aurora Drive", player), lambda state: room.can_reach(state))
|
||||
room = multiworld.get_location("Aurora Drive Room - Upgrade Console", player)
|
||||
set_rule(multiworld.get_location("Repair Aurora Drive", player),
|
||||
lambda state: room.can_reach(state))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
@@ -15,11 +15,11 @@ class SubnauticaTest(unittest.TestCase):
|
||||
self.assertGreater(self.scancutoff, id)
|
||||
|
||||
def testGroupAssociation(self):
|
||||
from worlds.subnautica import Items
|
||||
for item_id, item_data in Items.item_table.items():
|
||||
if item_data.type == Items.ItemType.group:
|
||||
from worlds.subnautica import items
|
||||
for item_id, item_data in items.item_table.items():
|
||||
if item_data.type == items.ItemType.group:
|
||||
with self.subTest(item=item_data.name):
|
||||
self.assertIn(item_id, Items.group_items)
|
||||
for item_id in Items.group_items:
|
||||
self.assertIn(item_id, items.group_items)
|
||||
for item_id in items.group_items:
|
||||
with self.subTest(item_id=item_id):
|
||||
self.assertEqual(Items.item_table[item_id].type, Items.ItemType.group)
|
||||
self.assertEqual(items.item_table[item_id].type, items.ItemType.group)
|
||||
|
||||
@@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity;
|
||||
Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh));
|
||||
Get a Life; Achievement; Life Fruit;
|
||||
Topped Off; Achievement; Life Fruit;
|
||||
Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3);
|
||||
Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3);
|
||||
|
||||
// Brimstone Elemental
|
||||
Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
|
||||
@@ -410,7 +410,7 @@ Scoria Bar; Calamity;
|
||||
Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar;
|
||||
Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode;
|
||||
Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array;
|
||||
Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem;
|
||||
Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem;
|
||||
|
||||
// Martian Madness
|
||||
Martian Madness; Location | Item; Wall of Flesh & Golem;
|
||||
|
||||
@@ -49,11 +49,9 @@ class TimespinnerWorld(World):
|
||||
|
||||
precalculated_weights: PreCalculatedWeights
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.precalculated_weights = PreCalculatedWeights(world, player)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player)
|
||||
|
||||
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
|
||||
if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
|
||||
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
|
||||
|
||||
@@ -44,7 +44,7 @@ them. Player settings page: [The Legend of Zelda Player Settings Page](/games/Th
|
||||
### Verifying your config file
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class WitnessWorld(World):
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000),
|
||||
'seed': self.random.randrange(0, 1000000),
|
||||
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
|
||||
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
|
||||
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
|
||||
|
||||
@@ -10,12 +10,13 @@ config file.
|
||||
Puzzles are randomly generated using the popular [Sigma Rando](https://github.com/sigma144/witness-randomizer).
|
||||
They are made to be similar to the original game, but with different solutions.
|
||||
|
||||
Ontop of that each puzzle symbol (Squares, Stars, Dots, etc.) is now an item.
|
||||
On top of that, each puzzle symbol (Squares, Stars, Dots, etc.) is now an item.
|
||||
Panels with puzzle symbols on them are now locked initially.
|
||||
|
||||
## What is a "check" in The Witness?
|
||||
|
||||
Solving the last panel in a row of panels or an important standalone panel will count as a check, and send out an item.
|
||||
It is also possible to add Environmental Puzzles into the location pool via the "Shuffle Environmental Puzzles" setting.
|
||||
|
||||
## What "items" can you unlock in The Witness?
|
||||
|
||||
@@ -32,7 +33,7 @@ By default, the audio logs scattered around the world will have 10 hints for you
|
||||
|
||||
Example: "Shipwreck Vault contains Triangles".
|
||||
|
||||
## The Jungle, Orchard, Forest and Color House aren't randomized. What gives?
|
||||
## The Jungle, Orchard, Forest and Color Bunker aren't randomized. What gives?
|
||||
|
||||
There are limitations to what can currently be randomized in The Witness.
|
||||
There is an option to turn these non-randomized panels off, called "disable_non_randomized" in your yaml file. This will also slightly change the activation requirement of certain panels, detailed [here](https://github.com/sigma144/witness-randomizer/wiki/Activation-Triggers).
|
||||
@@ -46,4 +47,4 @@ In this case, the generator will make its best attempt to adjust logic according
|
||||
One of the use cases of this could be to pre-open a specific door or pre-activate a single laser.
|
||||
|
||||
In "shuffle_EPs: obelisk_sides", any Environmental Puzzles in exclude_locations will be pre-completed and not considered for their Obelisk Side.
|
||||
If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely.
|
||||
If every Environmental Puzzle on an Obelisk Side is pre-completed, that side disappears from the location pool entirely.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user