Merge remote-tracking branch 'Main/main' into rework_accessibility

# Conflicts:
#	worlds/oot/Rules.py
This commit is contained in:
alwaysintreble
2023-10-27 07:48:03 -05:00
161 changed files with 4845 additions and 4138 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,5 @@
from test.TestBase import WorldTestBase
class BumpStikTestBase(WorldTestBase):
game = "Bumper Stickers"

View File

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

View File

@@ -11,6 +11,8 @@ from . import Options, data
class DLCQuestItem(Item):
game: str = "DLCQuest"
coins: int = 0
coin_suffix: str = ""
offset = 120_000

View File

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

View File

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

View File

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

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

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

View 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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 == "¿":

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"}

View 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"}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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