Merge branch 'ArchipelagoMW:main' into main

This commit is contained in:
CookieCat
2024-06-12 18:20:24 -04:00
committed by GitHub
109 changed files with 2568 additions and 572 deletions
+2 -2
View File
@@ -123,8 +123,8 @@ class WebWorldRegister(type):
assert group.options, "A custom defined Option Group must contain at least one Option."
# catch incorrectly titled versions of the prebuilt groups so they don't create extra groups
title_name = group.name.title()
if title_name in prebuilt_options:
group.name = title_name
assert title_name not in prebuilt_options or title_name == group.name, \
f"Prebuilt group name \"{group.name}\" must be \"{title_name}\""
if group.name == "Item & Location Options":
assert not any(option in item_and_loc_options for option in group.options), \
+83 -2
View File
@@ -1,8 +1,11 @@
import bisect
import logging
import pathlib
import weakref
from enum import Enum, auto
from typing import Optional, Callable, List, Iterable
from typing import Optional, Callable, List, Iterable, Tuple
from Utils import local_path
from Utils import local_path, open_filename
class Type(Enum):
@@ -49,8 +52,10 @@ class Component:
def __repr__(self):
return f"{self.__class__.__name__}({self.display_name})"
processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None):
global processes
import multiprocessing
@@ -58,6 +63,7 @@ def launch_subprocess(func: Callable, name: str = None):
process.start()
processes.add(process)
class SuffixIdentifier:
suffixes: Iterable[str]
@@ -77,6 +83,80 @@ def launch_textclient():
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
if not apworld_src:
apworld_src = open_filename('Select APWorld file to install', (('APWorld', ('.apworld',)),))
if not apworld_src:
# user closed menu
return
if not apworld_src.endswith(".apworld"):
raise Exception(f"Wrong file format, looking for .apworld. File identified: {apworld_src}")
apworld_path = pathlib.Path(apworld_src)
module_name = pathlib.Path(apworld_path.name).stem
try:
import zipfile
zipfile.ZipFile(apworld_path).open(module_name + "/__init__.py")
except ValueError as e:
raise Exception("Archive appears invalid or damaged.") from e
except KeyError as e:
raise Exception("Archive appears to not be an apworld. (missing __init__.py)") from e
import worlds
if worlds.user_folder is None:
raise Exception("Custom Worlds directory appears to not be writable.")
for world_source in worlds.world_sources:
if apworld_path.samefile(world_source.resolved_path):
# Note that this doesn't check if the same world is already installed.
# It only checks if the user is trying to install the apworld file
# that comes from the installation location (worlds or custom_worlds)
raise Exception(f"APWorld is already installed at {world_source.resolved_path}.")
# TODO: run generic test suite over the apworld.
# TODO: have some kind of version system to tell from metadata if the apworld should be compatible.
target = pathlib.Path(worlds.user_folder) / apworld_path.name
import shutil
shutil.copyfile(apworld_path, target)
# If a module with this name is already loaded, then we can't load it now.
# TODO: We need to be able to unload a world module,
# so the user can update a world without restarting the application.
found_already_loaded = False
for loaded_world in worlds.world_sources:
loaded_name = pathlib.Path(loaded_world.path).stem
if module_name == loaded_name:
found_already_loaded = True
break
if found_already_loaded:
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
"so a Launcher restart is required to use the new installation.")
world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source)
world_source.load()
return apworld_path, target
def install_apworld(apworld_path: str = "") -> None:
try:
res = _install_apworld(apworld_path)
if res is None:
logging.info("Aborting APWorld installation.")
return
source, target = res
except Exception as e:
import Utils
Utils.messagebox(e.__class__.__name__, str(e), error=True)
logging.exception(e)
else:
import Utils
logging.info(f"Installed APWorld successfully, copied {source} to {target}.")
Utils.messagebox("Install complete.", f"Installed APWorld from {source}.")
components: List[Component] = [
# Launcher
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
@@ -84,6 +164,7 @@ components: List[Component] = [
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
Component('Generate', 'Generate', cli=True),
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
+15 -5
View File
@@ -1,16 +1,22 @@
import importlib
import importlib.util
import logging
import os
import sys
import warnings
import zipimport
import time
import dataclasses
from typing import Dict, List, TypedDict, Optional
from typing import Dict, List, TypedDict
from Utils import local_path, user_path
local_folder = os.path.dirname(__file__)
user_folder = user_path("worlds") if user_path() != local_path() else None
user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
try:
os.makedirs(user_folder, exist_ok=True)
except OSError: # can't access/write?
user_folder = None
__all__ = {
"network_data_package",
@@ -44,7 +50,7 @@ class WorldSource:
path: str # typically relative path from this module
is_zip: bool = False
relative: bool = True # relative to regular world import folder
time_taken: Optional[float] = None
time_taken: float = -1.0
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
@@ -88,7 +94,6 @@ class WorldSource:
print(f"Could not load world {self}:", file=file_like)
traceback.print_exc(file=file_like)
file_like.seek(0)
import logging
logging.exception(file_like.read())
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
return False
@@ -103,7 +108,12 @@ for folder in (folder for folder in (user_folder, local_folder) if folder):
if not entry.name.startswith(("_", ".")):
file_name = entry.name if relative else os.path.join(folder, entry.name)
if entry.is_dir():
world_sources.append(WorldSource(file_name, relative=relative))
if os.path.isfile(os.path.join(entry.path, '__init__.py')):
world_sources.append(WorldSource(file_name, relative=relative))
elif os.path.isfile(os.path.join(entry.path, '__init__.pyc')):
world_sources.append(WorldSource(file_name, relative=relative))
else:
logging.warning(f"excluding {entry.name} from world sources because it has no __init__.py")
elif entry.is_file() and entry.name.endswith(".apworld"):
world_sources.append(WorldSource(file_name, is_zip=True, relative=relative))
+1
View File
@@ -168,6 +168,7 @@ async def _game_watcher(ctx: BizHawkClientContext):
ctx.auth = None
ctx.username = None
ctx.client_handler = None
ctx.finished_game = False
await ctx.disconnect(False)
ctx.rom_hash = rom_hash
+6 -1
View File
@@ -28,6 +28,11 @@ class kill_switch:
logger.debug("kill_switch: Add switch")
cls._to_kill.append(value)
@classmethod
def kill(cls, value):
logger.info(f"kill_switch: Process cleanup for 1 process")
value._clean(verbose=False)
@classmethod
def kill_all(cls):
logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes")
@@ -116,7 +121,7 @@ class SC2Process:
async def __aexit__(self, *args):
logger.exception("async exit")
await self._close_connection()
kill_switch.kill_all()
kill_switch.kill(self)
signal.signal(signal.SIGINT, signal.SIG_DFL)
@property
+19 -18
View File
@@ -2,7 +2,8 @@ from __future__ import annotations
from typing import Dict
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
from dataclasses import dataclass
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range):
@@ -223,22 +224,22 @@ class StartCastle(Choice):
option_white = 2
default = option_yellow
@dataclass
class AdventureOptions(PerGameCommonOptions):
dragon_slay_check: DragonSlayCheck
death_link: DeathLink
bat_logic: BatLogic
freeincarnate_max: FreeincarnateMax
dragon_rando_type: DragonRandoType
connector_multi_slot: ConnectorMultiSlot
yorgle_speed: YorgleStartingSpeed
yorgle_min_speed: YorgleMinimumSpeed
grundle_speed: GrundleStartingSpeed
grundle_min_speed: GrundleMinimumSpeed
rhindle_speed: RhindleStartingSpeed
rhindle_min_speed: RhindleMinimumSpeed
difficulty_switch_a: DifficultySwitchA
difficulty_switch_b: DifficultySwitchB
start_castle: StartCastle
adventure_option_definitions: Dict[str, type(Option)] = {
"dragon_slay_check": DragonSlayCheck,
"death_link": DeathLink,
"bat_logic": BatLogic,
"freeincarnate_max": FreeincarnateMax,
"dragon_rando_type": DragonRandoType,
"connector_multi_slot": ConnectorMultiSlot,
"yorgle_speed": YorgleStartingSpeed,
"yorgle_min_speed": YorgleMinimumSpeed,
"grundle_speed": GrundleStartingSpeed,
"grundle_min_speed": GrundleMinimumSpeed,
"rhindle_speed": RhindleStartingSpeed,
"rhindle_min_speed": RhindleMinimumSpeed,
"difficulty_switch_a": DifficultySwitchA,
"difficulty_switch_b": DifficultySwitchB,
"start_castle": StartCastle,
}
+3 -2
View File
@@ -1,4 +1,5 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from Options import PerGameCommonOptions
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
@@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call
connect(world, player, target, source, rule, True)
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
menu = Region("Menu", player, multiworld)
@@ -74,7 +75,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
multiworld.regions.append(credits_room_far_side)
dragon_slay_check = multiworld.dragon_slay_check[player].value
dragon_slay_check = options.dragon_slay_check.value
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
for name, location_data in location_table.items():
+2 -2
View File
@@ -6,7 +6,7 @@ from BaseClasses import LocationProgressType
def set_rules(self) -> None:
world = self.multiworld
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic
set_rule(world.get_entrance("YellowCastlePort", self.player),
lambda state: state.has("Yellow Key", self.player))
@@ -28,7 +28,7 @@ def set_rules(self) -> None:
lambda state: state.has("Bridge", self.player) or
state.has("Magnet", self.player))
dragon_slay_check = world.dragon_slay_check[self.player].value
dragon_slay_check = self.options.dragon_slay_check.value
if dragon_slay_check:
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
set_rule(world.get_location("Slay Yorgle", self.player),
+19 -18
View File
@@ -15,7 +15,8 @@ from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
@@ -109,7 +110,7 @@ class AdventureWorld(World):
game: ClassVar[str] = "Adventure"
web: ClassVar[WebWorld] = AdventureWeb()
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
options_dataclass = AdventureOptions
settings: ClassVar[AdventureSettings]
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
@@ -149,18 +150,18 @@ class AdventureWorld(World):
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
self.rom_name.extend([0] * (21 - len(self.rom_name)))
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
self.start_castle = self.multiworld.start_castle[self.player].value
self.dragon_rando_type = self.options.dragon_rando_type.value
self.dragon_slay_check = self.options.dragon_slay_check.value
self.connector_multi_slot = self.options.connector_multi_slot.value
self.yorgle_speed = self.options.yorgle_speed.value
self.yorgle_min_speed = self.options.yorgle_min_speed.value
self.grundle_speed = self.options.grundle_speed.value
self.grundle_min_speed = self.options.grundle_min_speed.value
self.rhindle_speed = self.options.rhindle_speed.value
self.rhindle_min_speed = self.options.rhindle_min_speed.value
self.difficulty_switch_a = self.options.difficulty_switch_a.value
self.difficulty_switch_b = self.options.difficulty_switch_b.value
self.start_castle = self.options.start_castle.value
self.created_items = 0
if self.dragon_slay_check == 0:
@@ -227,7 +228,7 @@ class AdventureWorld(World):
extra_filler_count = num_locations - self.created_items
# traps would probably go here, if enabled
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
freeincarnate_max = self.options.freeincarnate_max.value
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
self.created_items += actual_freeincarnates
@@ -247,7 +248,7 @@ class AdventureWorld(World):
self.created_items += 1
def create_regions(self) -> None:
create_regions(self.multiworld, self.player, self.dragon_rooms)
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
set_rules = set_rules
@@ -354,7 +355,7 @@ class AdventureWorld(World):
auto_collect_locations: [AdventureAutoCollectLocation] = []
local_item_to_location: {int, int} = {}
bat_no_touch_locs: [LocationData] = []
bat_logic: int = self.multiworld.bat_logic[self.player].value
bat_logic: int = self.options.bat_logic.value
try:
rom_deltas: { int, int } = {}
self.place_dragons(rom_deltas)
@@ -421,7 +422,7 @@ class AdventureWorld(World):
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
rom_deltas[item_position_data_start] = 0xff
if self.multiworld.connector_multi_slot[self.player].value:
if self.options.connector_multi_slot.value:
rom_deltas[connector_port_offset] = (self.player & 0xff)
else:
rom_deltas[connector_port_offset] = 0
+3 -3
View File
@@ -185,7 +185,7 @@ class AquariaLocations:
"Mithalas City, second bulb at the end of the top path": 698040,
"Mithalas City, bulb in the top path": 698036,
"Mithalas City, Mithalas Pot": 698174,
"Mithalas City, urn in the Cathedral flower tube entrance": 698128,
"Mithalas City, urn in the Castle flower tube entrance": 698128,
}
locations_mithalas_city_fishpass = {
@@ -246,7 +246,7 @@ class AquariaLocations:
"Kelp Forest top left area, bulb in the bottom left clearing": 698044,
"Kelp Forest top left area, bulb in the path down from the top left clearing": 698045,
"Kelp Forest top left area, bulb in the top left clearing": 698046,
"Kelp Forest top left, Jelly Egg": 698185,
"Kelp Forest top left area, Jelly Egg": 698185,
}
locations_forest_tl_fp = {
@@ -332,7 +332,7 @@ class AquariaLocations:
}
locations_veil_tr_l = {
"The Veil top right area, bulb in the top of the waterfall": 698080,
"The Veil top right area, bulb at the top of the waterfall": 698080,
"The Veil top right area, Transturtle": 698210,
}
+4 -3
View File
@@ -771,6 +771,7 @@ class AquariaRegions:
self.__connect_regions("Sunken City left area", "Sunken City boss area",
self.sunken_city_l, self.sunken_city_boss,
lambda state: _has_beast_form(state, self.player) and
_has_sun_form(state, self.player) and
_has_energy_form(state, self.player) and
_has_bind_song(state, self.player))
@@ -983,7 +984,7 @@ class AquariaRegions:
lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player),
lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player),
add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player),
lambda state: _has_damaging_item(state, self.player))
add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player),
lambda state: _has_damaging_item(state, self.player))
@@ -1023,7 +1024,7 @@ class AquariaRegions:
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player),
add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player),
lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player))
def __adjusting_under_rock_location(self) -> None:
@@ -1175,7 +1176,7 @@ class AquariaRegions:
self.multiworld.get_location("Sun Worm path, second cliff bulb",
self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall",
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
self.player).item_rule =\
lambda item: item.classification != ItemClassification.progression
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
+4 -8
View File
@@ -167,14 +167,10 @@ class AquariaWorld(World):
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items():
if name in precollected:
precollected.remove(name)
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
else:
if name not in self.exclude:
for i in range(data.count):
item = self.create_item(name)
self.multiworld.itempool.append(item)
if name not in self.exclude:
for i in range(data.count):
item = self.create_item(name)
self.multiworld.itempool.append(item)
def set_rules(self) -> None:
"""
+3 -3
View File
@@ -56,7 +56,7 @@ after_home_water_locations = [
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Cathedral flower tube entrance",
"Mithalas City, urn in the Castle flower tube entrance",
"Mithalas City, Doll",
"Mithalas City, urn inside a home fish pass",
"Mithalas City Castle, bulb in the flesh hole",
@@ -93,7 +93,7 @@ after_home_water_locations = [
"Kelp Forest top left area, bulb in the bottom left clearing",
"Kelp Forest top left area, bulb in the path down from the top left clearing",
"Kelp Forest top left area, bulb in the top left clearing",
"Kelp Forest top left, Jelly Egg",
"Kelp Forest top left area, Jelly Egg",
"Kelp Forest top left area, bulb close to the Verse Egg",
"Kelp Forest top left area, Verse Egg",
"Kelp Forest top right area, bulb under the rock in the right path",
@@ -125,7 +125,7 @@ after_home_water_locations = [
"Turtle cave, Urchin Costume",
"The Veil top right area, bulb in the middle of the wall jump cliff",
"The Veil top right area, Golden Starfish",
"The Veil top right area, bulb in the top of the waterfall",
"The Veil top right area, bulb at the top of the waterfall",
"The Veil top right area, Transturtle",
"The Veil bottom area, bulb in the left path",
"The Veil bottom area, bulb in the spirit path",
@@ -20,14 +20,14 @@ class BeastFormAccessTest(AquariaTestBase):
"Mithalas City, second bulb at the end of the top path",
"Mithalas City, bulb in the top path",
"Mithalas City, Mithalas Pot",
"Mithalas City, urn in the Cathedral flower tube entrance",
"Mithalas City, urn in the Castle flower tube entrance",
"Mermog cave, Piranha Egg",
"Mithalas Cathedral, Mithalan Dress",
"Turtle cave, bulb in Bubble Cliff",
"Turtle cave, Urchin Costume",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The Veil top right area, bulb in the top of the waterfall",
"The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The Veil top right area, bulb in the top of the waterfall",
"The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
@@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
"Final Boss area, bulb in the boss third form room",
"Sun Worm path, first cliff bulb",
"Sun Worm path, second cliff bulb",
"The Veil top right area, bulb in the top of the waterfall",
"The Veil top right area, bulb at the top of the waterfall",
"Bubble Cave, bulb in the left cave wall",
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
"Bubble Cave, Verse Egg",
@@ -18,6 +18,9 @@ class SunFormAccessTest(AquariaTestBase):
"Abyss right area, bulb behind the rock in the whale room",
"Octopus Cave, Dumbo Egg",
"Beating Octopus Prime",
"Sunken City, bulb on top of the boss area",
"Beating the Golem",
"Sunken City cleared",
"Final Boss area, bulb in the boss third form room",
"Objective complete"
]
+2 -2
View File
@@ -109,7 +109,7 @@ class BombRushCyberfunkWorld(World):
def create_items(self):
rep_locations: int = 87
if self.options.skip_polo_photos:
rep_locations -= 18
rep_locations -= 17
self.options.total_rep.round_to_nearest_step()
rep_counts = self.options.total_rep.get_rep_item_counts(self.random, rep_locations)
@@ -157,7 +157,7 @@ class BombRushCyberfunkWorld(World):
self.get_region(n).add_exits(region_exits[n])
for index, loc in enumerate(location_table):
if self.options.skip_polo_photos and "Polo" in loc["name"]:
if self.options.skip_polo_photos and "Polo" in loc["game_id"]:
continue
stage: Region = self.get_region(loc["stage"])
stage.add_locations({loc["name"]: base_id + index})
+1
View File
@@ -64,3 +64,4 @@ item_name_groups = ({
})
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}
item_name_groups['Skills'] |= item_name_groups['Vertical'] | item_name_groups['Horizontal']
+5 -1
View File
@@ -2,6 +2,7 @@ import typing
import re
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
from schema import And, Schema, Optional
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink
from .Charms import vanilla_costs, names as charm_names
@@ -212,7 +213,7 @@ class MinimumEggPrice(Range):
Only takes effect if the EggSlotShops option is greater than 0."""
display_name = "Minimum Egg Price"
range_start = 1
range_end = 21
range_end = 20
default = 1
@@ -296,6 +297,9 @@ class PlandoCharmCosts(OptionDict):
This is set after any random Charm Notch costs, if applicable."""
display_name = "Charm Notch Cost Plando"
valid_keys = frozenset(charm_names)
schema = Schema({
Optional(name): And(int, lambda n: 6 >= n >= 0) for name in charm_names
})
def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]:
for name, cost in self.value.items():
+2 -2
View File
@@ -308,14 +308,14 @@ class CorSkipToggle(Toggle):
Full Cor Skip is also affected by this Toggle.
"""
display_name = "CoR Skip Toggle."
display_name = "CoR Skip Toggle"
default = False
class CustomItemPoolQuantity(ItemDict):
"""Add more of an item into the itempool. Note: You cannot take out items from the pool."""
display_name = "Custom Item Pool"
verify_item_name = True
valid_keys = default_itempool_option.keys()
default = default_itempool_option
+3 -3
View File
@@ -430,13 +430,13 @@ class KH2World(World):
"""
for item, value in self.options.start_inventory.value.items():
if item in ActionAbility_Table \
or item in SupportAbility_Table or exclusion_item_table["StatUps"] \
or item in SupportAbility_Table or item in exclusion_item_table["StatUps"] \
or item in DonaldAbility_Table or item in GoofyAbility_Table:
# cannot have more than the quantity for abilties
if value > item_dictionary_table[item].quantity:
logging.info(
f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}"
f"Changing the amount to the max amount")
f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}."
f" Changing the amount to the max amount")
value = item_dictionary_table[item].quantity
self.item_quantity_dict[item] -= value
+1 -1
View File
@@ -5,7 +5,7 @@ from schema import And, Optional, Or, Schema
from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
class MessengerAccessibility(Accessibility):
+1 -2
View File
@@ -306,8 +306,7 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
if world.options.scale_stats:
patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
if world.options.xp_multiplier:
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
if world.options.tattle_hp:
patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
+19 -11
View File
@@ -22,12 +22,15 @@ class MuseDashCollections:
]
MUSE_PLUS_DLC: str = "Muse Plus"
# Ordering matters for webhost. Order goes: Muse Plus, Time Limited Muse Plus Dlcs, Paid Dlcs
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
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
"MSR Anthology", # Now no longer available.
MUSE_PLUS_DLC,
"CHUNITHM COURSE MUSE", # Part of Muse Plus. Goes away 22nd May 2027.
"maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
"MSR Anthology", # Now no longer available.
"Miku in Museland", # Paid DLC not included in Muse Plus
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
]
DIFF_OVERRIDES: List[str] = [
@@ -50,7 +53,7 @@ class MuseDashCollections:
song_items: Dict[str, SongData] = {}
song_locations: Dict[str, int] = {}
vfx_trap_items: Dict[str, int] = {
trap_items: Dict[str, int] = {
"Bad Apple Trap": STARTING_CODE + 1,
"Pixelate Trap": STARTING_CODE + 2,
"Ripple Trap": STARTING_CODE + 3,
@@ -58,14 +61,16 @@ class MuseDashCollections:
"Chromatic Aberration Trap": STARTING_CODE + 5,
"Background Freeze Trap": STARTING_CODE + 6,
"Gray Scale Trap": STARTING_CODE + 7,
"Focus Line Trap": STARTING_CODE + 10,
}
sfx_trap_items: Dict[str, int] = {
"Nyaa SFX Trap": STARTING_CODE + 8,
"Error SFX Trap": STARTING_CODE + 9,
"Focus Line Trap": STARTING_CODE + 10,
}
sfx_trap_items: List[str] = [
"Nyaa SFX Trap",
"Error SFX Trap",
]
filler_items: Dict[str, int] = {
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
"Miss To Great (5 Pack)": STARTING_CODE + 31,
@@ -78,7 +83,7 @@ class MuseDashCollections:
"Extra Life": 1,
}
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
item_names_to_id: ChainMap = ChainMap({}, filler_items, trap_items)
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
def __init__(self) -> None:
@@ -171,6 +176,9 @@ class MuseDashCollections:
return filtered_list
def filter_songs_to_dlc(self, song_list: List[str], dlc_songs: Set[str]) -> List[str]:
return [song for song in song_list if self.song_matches_dlc_filter(self.song_items[song], dlc_songs)]
def song_matches_dlc_filter(self, song: SongData, dlc_songs: Set[str]) -> bool:
if song.album in self.FREE_ALBUMS:
return True
+74 -35
View File
@@ -1,26 +1,26 @@
from typing import Dict
from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions
from Options import Toggle, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions, OptionGroup, Removed
from dataclasses import dataclass
from .MuseDashCollection import MuseDashCollections
class AllowJustAsPlannedDLCSongs(Toggle):
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
display_name = "Allow [Muse Plus] DLC Songs"
class DLCMusicPacks(OptionSet):
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
"""
Choose which DLC Packs will be included in the pool of chooseable songs.
Note: The [Just As Planned] DLC contains all [Muse Plus] songs.
"""
display_name = "DLC Packs"
default = {}
valid_keys = [dlc for dlc in MuseDashCollections.DLC]
class StreamerModeEnabled(Toggle):
"""In Muse Dash, an option named 'Streamer Mode' removes songs which may trigger copyright issues when streaming.
If this is enabled, only songs available under Streamer Mode will be available for randomization."""
"""
In Muse Dash, an option named 'Streamer Mode' removes songs which may trigger copyright issues when streaming.
If this is enabled, only songs available under Streamer Mode will be available for randomization.
"""
display_name = "Streamer Mode Only Songs"
@@ -33,7 +33,8 @@ class StartingSongs(Range):
class AdditionalSongs(Range):
"""The total number of songs that will be placed in the randomization pool.
"""
The total number of songs that will be placed in the randomization pool.
- This does not count any starting songs or the goal song.
- The final song count may be lower due to other settings.
"""
@@ -44,7 +45,8 @@ class AdditionalSongs(Range):
class DifficultyMode(Choice):
"""Ensures that at any chosen song has at least 1 value falling within these values.
"""
Ensures that at any chosen song has at least 1 value falling within these values.
- Any: All songs are available
- Easy: 1, 2 or 3
- Medium: 4, 5
@@ -66,8 +68,11 @@ class DifficultyMode(Choice):
# Todo: Investigate options to make this non randomizable
class DifficultyModeOverrideMin(Range):
"""Ensures that 1 difficulty has at least 1 this value or higher per song.
- Difficulty Mode must be set to Manual."""
"""
Ensures that 1 difficulty has at least 1 this value or higher per song.
Note: Difficulty Mode must be set to Manual.
"""
display_name = "Manual Difficulty Min"
range_start = 1
range_end = 11
@@ -76,8 +81,11 @@ class DifficultyModeOverrideMin(Range):
# Todo: Investigate options to make this non randomizable
class DifficultyModeOverrideMax(Range):
"""Ensures that 1 difficulty has at least 1 this value or lower per song.
- Difficulty Mode must be set to Manual."""
"""
Ensures that 1 difficulty has at least 1 this value or lower per song.
Note: Difficulty Mode must be set to Manual.
"""
display_name = "Manual Difficulty Max"
range_start = 1
range_end = 11
@@ -85,7 +93,8 @@ class DifficultyModeOverrideMax(Range):
class GradeNeeded(Choice):
"""Completing a song will require a grade of this value or higher in order to unlock items.
"""
Completing a song will require a grade of this value or higher in order to unlock items.
The grades are as follows:
- Silver S (SS): >= 95% accuracy
- Pink S (S): >= 90% accuracy
@@ -104,7 +113,9 @@ class GradeNeeded(Choice):
class MusicSheetCountPercentage(Range):
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
"""
Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
"""
range_start = 10
@@ -121,19 +132,18 @@ class MusicSheetWinCountPercentage(Range):
display_name = "Music Sheets Needed to Win"
class TrapTypes(Choice):
"""This controls the types of traps that can be added to the pool.
class ChosenTraps(OptionSet):
"""
This controls the types of traps that can be added to the pool.
- Traps last the length of a song, or until you die.
- VFX Traps consist of visual effects that play over the song. (i.e. Grayscale.)
- SFX Traps consist of changing your sfx setting to one possibly more annoying sfx.
Traps last the length of a song, or until you die.
Note: SFX traps are only available if [Just as Planned] DLC songs are enabled.
"""
display_name = "Available Trap Types"
option_None = 0
option_VFX = 1
option_SFX = 2
option_All = 3
default = 3
display_name = "Chosen Traps"
default = {}
valid_keys = {trap for trap in MuseDashCollections.trap_items.keys()}
class TrapCountPercentage(Range):
@@ -145,24 +155,49 @@ class TrapCountPercentage(Range):
class IncludeSongs(ItemSet):
"""Any song listed here will be guaranteed to be included as part of the seed.
- Difficulty options will be skipped for these songs.
- If there being too many included songs, songs will be randomly chosen without regard for difficulty.
- If you want these songs immediately, use start_inventory instead.
"""
These songs will be guaranteed to show up within the seed.
- You must have the DLC enabled to play these songs.
- Difficulty options will not affect these songs.
- If there are too many included songs, this will act as a whitelist ignoring song difficulty.
"""
verify_item_name = True
display_name = "Include Songs"
class ExcludeSongs(ItemSet):
"""Any song listed here will be excluded from being a part of the seed."""
"""
These songs will be guaranteed to not show up within the seed.
Note: Does not affect songs within the "Include Songs" list.
"""
verify_item_name = True
display_name = "Exclude Songs"
md_option_groups = [
OptionGroup("Song Choice", [
DLCMusicPacks,
StreamerModeEnabled,
IncludeSongs,
ExcludeSongs,
]),
OptionGroup("Difficulty", [
GradeNeeded,
DifficultyMode,
DifficultyModeOverrideMin,
DifficultyModeOverrideMax,
DeathLink,
]),
OptionGroup("Traps", [
ChosenTraps,
TrapCountPercentage,
]),
]
@dataclass
class MuseDashOptions(PerGameCommonOptions):
allow_just_as_planned_dlc_songs: AllowJustAsPlannedDLCSongs
dlc_packs: DLCMusicPacks
streamer_mode_enabled: StreamerModeEnabled
starting_song_count: StartingSongs
@@ -173,8 +208,12 @@ class MuseDashOptions(PerGameCommonOptions):
grade_needed: GradeNeeded
music_sheet_count_percentage: MusicSheetCountPercentage
music_sheet_win_count_percentage: MusicSheetWinCountPercentage
available_trap_types: TrapTypes
chosen_traps: ChosenTraps
trap_count_percentage: TrapCountPercentage
death_link: DeathLink
include_songs: IncludeSongs
exclude_songs: ExcludeSongs
# Removed
allow_just_as_planned_dlc_songs: Removed
available_trap_types: Removed
+3 -3
View File
@@ -3,7 +3,7 @@
MuseDashPresets: Dict[str, Dict[str, Any]] = {
# An option to support Short Sync games. 40 songs.
"No DLC - Short": {
"allow_just_as_planned_dlc_songs": False,
"dlc_packs": [],
"starting_song_count": 5,
"additional_song_count": 34,
"music_sheet_count_percentage": 20,
@@ -11,7 +11,7 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
},
# An option to support Short Sync games but adds variety. 40 songs.
"DLC - Short": {
"allow_just_as_planned_dlc_songs": True,
"dlc_packs": ["Muse Plus"],
"starting_song_count": 5,
"additional_song_count": 34,
"music_sheet_count_percentage": 20,
@@ -19,7 +19,7 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
},
# An option to support Longer Sync/Async games. 100 songs.
"DLC - Long": {
"allow_just_as_planned_dlc_songs": True,
"dlc_packs": ["Muse Plus"],
"starting_song_count": 8,
"additional_song_count": 91,
"music_sheet_count_percentage": 20,
+23 -36
View File
@@ -1,10 +1,10 @@
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Region, Item, ItemClassification, Entrance, Tutorial
from typing import List, ClassVar, Type
from BaseClasses import Region, Item, ItemClassification, Tutorial
from typing import List, ClassVar, Type, Set
from math import floor
from Options import PerGameCommonOptions
from .Options import MuseDashOptions
from .Options import MuseDashOptions, md_option_groups
from .Items import MuseDashSongItem, MuseDashFixedItem
from .Locations import MuseDashLocation
from .MuseDashCollection import MuseDashCollections
@@ -35,6 +35,7 @@ class MuseDashWebWorld(WebWorld):
tutorials = [setup_en, setup_es]
options_presets = MuseDashPresets
option_groups = md_option_groups
class MuseDashWorld(World):
@@ -72,8 +73,6 @@ class MuseDashWorld(World):
def generate_early(self):
dlc_songs = {key for key in self.options.dlc_packs.value}
if self.options.allow_just_as_planned_dlc_songs.value:
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()
@@ -88,7 +87,7 @@ class MuseDashWorld(World):
available_song_keys = self.md_collection.get_songs_with_settings(
dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
available_song_keys = self.handle_plando(available_song_keys)
available_song_keys = self.handle_plando(available_song_keys, dlc_songs)
count_needed_for_start = max(0, starter_song_count - len(self.starting_songs))
if len(available_song_keys) + len(self.included_songs) >= count_needed_for_start + 11:
@@ -109,7 +108,7 @@ class MuseDashWorld(World):
for song in self.starting_songs:
self.multiworld.push_precollected(self.create_item(song))
def handle_plando(self, available_song_keys: List[str]) -> List[str]:
def handle_plando(self, available_song_keys: List[str], dlc_songs: Set[str]) -> List[str]:
song_items = self.md_collection.song_items
start_items = self.options.start_inventory.value.keys()
@@ -117,7 +116,9 @@ class MuseDashWorld(World):
exclude_songs = self.options.exclude_songs.value
self.starting_songs = [s for s in start_items if s in song_items]
self.starting_songs = self.md_collection.filter_songs_to_dlc(self.starting_songs, dlc_songs)
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
self.included_songs = self.md_collection.filter_songs_to_dlc(self.included_songs, dlc_songs)
return [s for s in available_song_keys if s not in start_items
and s not in include_songs and s not in exclude_songs]
@@ -148,7 +149,7 @@ class MuseDashWorld(World):
self.victory_song_name = available_song_keys[chosen_song - included_song_count]
del available_song_keys[chosen_song - included_song_count]
# Next, make sure the starting songs are fufilled
# Next, make sure the starting songs are fulfilled
if len(self.starting_songs) < starting_song_count:
for _ in range(len(self.starting_songs), starting_song_count):
if len(available_song_keys) > 0:
@@ -156,7 +157,7 @@ class MuseDashWorld(World):
else:
self.starting_songs.append(self.included_songs.pop())
# Then attempt to fufill any remaining songs for interim songs
# Then attempt to fulfill any remaining songs for interim songs
if len(self.included_songs) < additional_song_count:
for _ in range(len(self.included_songs), self.options.additional_song_count):
if len(available_song_keys) <= 0:
@@ -174,11 +175,7 @@ class MuseDashWorld(World):
if filler:
return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
trap = self.md_collection.vfx_trap_items.get(name)
if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
trap = self.md_collection.sfx_trap_items.get(name)
trap = self.md_collection.trap_items.get(name)
if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
@@ -252,9 +249,7 @@ class MuseDashWorld(World):
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
song_select_region = Region("Song Select", self.player, self.multiworld)
self.multiworld.regions += [menu_region, song_select_region]
menu_region.connect(song_select_region)
self.multiworld.regions += [menu_region]
# Make a collection of all songs available for this rando.
# 1. All starting songs
@@ -268,35 +263,27 @@ class MuseDashWorld(World):
self.random.shuffle(included_song_copy)
all_selected_locations.extend(included_song_copy)
# Make a region per song/album, then adds 1-2 item locations to them
# Adds 2 item locations per song/album to the menu region.
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
region = Region(name, self.player, self.multiworld)
self.multiworld.regions.append(region)
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region)
loc1.access_rule = lambda state, place=name: state.has(place, self.player)
menu_region.locations.append(loc1)
# Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
region.add_locations({
name + "-0": self.md_collection.song_locations[name + "-0"],
name + "-1": self.md_collection.song_locations[name + "-1"]
}, MuseDashLocation)
loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region)
loc2.access_rule = lambda state, place=name: state.has(place, self.player)
menu_region.locations.append(loc2)
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
def get_available_traps(self) -> List[str]:
sfx_traps_available = self.options.allow_just_as_planned_dlc_songs.value
full_trap_list = self.md_collection.trap_items.keys()
if self.md_collection.MUSE_PLUS_DLC not in self.options.dlc_packs.value:
full_trap_list = [trap for trap in full_trap_list if trap not in self.md_collection.sfx_trap_items]
trap_list = []
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 sfx_traps_available and self.options.available_trap_types.value & 2 != 0:
trap_list += self.md_collection.sfx_trap_items.keys()
return trap_list
return [trap for trap in full_trap_list if trap in self.options.chosen_traps.value]
def get_trap_count(self) -> int:
multiplier = self.options.trap_count_percentage.value / 100.0
+9 -8
View File
@@ -9,25 +9,26 @@ class CollectionsTest(unittest.TestCase):
for name in collection.song_items.keys():
for c in name:
# This is taken directly from OoT. Represents the generally excepted characters.
if (0x20 <= ord(c) < 0x7e):
if 0x20 <= ord(c) < 0x7e:
continue
bad_names.append(name)
break
self.assertEqual(len(bad_names), 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}")
self.assertEqual(len(bad_names), 0,
f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}")
def test_ids_dont_change(self) -> None:
collection = MuseDashCollections()
itemsBefore = {name: code for name, code in collection.item_names_to_id.items()}
locationsBefore = {name: code for name, code in collection.location_names_to_id.items()}
items_before = {name: code for name, code in collection.item_names_to_id.items()}
locations_before = {name: code for name, code in collection.location_names_to_id.items()}
collection.__init__()
itemsAfter = {name: code for name, code in collection.item_names_to_id.items()}
locationsAfter = {name: code for name, code in collection.location_names_to_id.items()}
items_after = {name: code for name, code in collection.item_names_to_id.items()}
locations_after = {name: code for name, code in collection.location_names_to_id.items()}
self.assertDictEqual(itemsBefore, itemsAfter, "Item ID changed after secondary init.")
self.assertDictEqual(locationsBefore, locationsAfter, "Location ID changed after secondary init.")
self.assertDictEqual(items_before, items_after, "Item ID changed after secondary init.")
self.assertDictEqual(locations_before, locations_after, "Location ID changed after secondary init.")
def test_free_dlc_included_in_base_songs(self) -> None:
collection = MuseDashCollections()
+12 -12
View File
@@ -3,31 +3,31 @@ from . import MuseDashTestBase
class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
muse_dash_world = self.get_world()
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
difficulty_choice = muse_dash_world.options.song_difficulty_mode
difficulty_min = muse_dash_world.options.song_difficulty_min
difficulty_max = muse_dash_world.options.song_difficulty_max
def test_range(inputRange, lower, upper):
self.assertEqual(inputRange[0], lower)
self.assertEqual(inputRange[1], upper)
def test_range(input_range, lower, upper):
self.assertEqual(input_range[0], lower)
self.assertEqual(input_range[1], upper)
songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, inputRange[0], inputRange[1])
songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, input_range[0], input_range[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]):
if song.easy is not None and input_range[0] <= song.easy <= input_range[1]:
continue
if (song.hard is not None and inputRange[0] <= song.hard <= inputRange[1]):
if song.hard is not None and input_range[0] <= song.hard <= input_range[1]:
continue
if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]):
if song.master is not None and input_range[0] <= song.master <= input_range[1]:
continue
self.fail(f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'")
self.fail(f"Invalid song '{songKey}' was given for range '{input_range[0]} to {input_range[1]}'")
#auto ranges
# auto ranges
difficulty_choice.value = 0
test_range(muse_dash_world.get_difficulty_range(), 0, 12)
difficulty_choice.value = 1
@@ -61,7 +61,7 @@ class DifficultyRanges(MuseDashTestBase):
test_range(muse_dash_world.get_difficulty_range(), 4, 6)
def test_songs_have_difficulty(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
muse_dash_world = self.get_world()
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
@@ -73,4 +73,4 @@ class DifficultyRanges(MuseDashTestBase):
f"Song '{song_name}' difficulty not set when it should be.")
else:
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
f"Song '{song_name}' difficulty not set when it should be.")
f"Song '{song_name}' difficulty not set when it should be.")
+32 -7
View File
@@ -4,7 +4,32 @@ from . import MuseDashTestBase
class TestPlandoSettings(MuseDashTestBase):
options = {
"additional_song_count": 15,
"allow_just_as_planned_dlc_songs": True,
"dlc_packs": {"Muse Plus"},
"include_songs": [
"Lunatic",
"Out of Sense",
"Magic Knight Girl",
]
}
def test_included_songs_didnt_grow_item_count(self) -> None:
muse_dash_world = self.get_world()
self.assertEqual(len(muse_dash_world.included_songs), 15, "Logical songs size grew when it shouldn't.")
def test_included_songs_plando(self) -> None:
muse_dash_world = self.get_world()
songs = muse_dash_world.included_songs.copy()
songs.append(muse_dash_world.victory_song_name)
self.assertIn("Lunatic", songs, "Logical songs is missing a plando song: Lunatic")
self.assertIn("Out of Sense", songs, "Logical songs is missing a plando song: Out of Sense")
self.assertIn("Magic Knight Girl", songs, "Logical songs is missing a plando song: Magic Knight Girl")
class TestFilteredPlandoSettings(MuseDashTestBase):
options = {
"additional_song_count": 15,
"dlc_packs": {"MSR Anthology"},
"include_songs": [
"Operation Blade",
"Autumn Moods",
@@ -13,15 +38,15 @@ class TestPlandoSettings(MuseDashTestBase):
}
def test_included_songs_didnt_grow_item_count(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
self.assertEqual(len(muse_dash_world.included_songs), 15,
f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}")
muse_dash_world = self.get_world()
self.assertEqual(len(muse_dash_world.included_songs), 15, "Logical songs size grew when it shouldn't.")
def test_included_songs_plando(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
# Tests for excluding included songs when the right dlc isn't enabled
def test_filtered_included_songs_plando(self) -> None:
muse_dash_world = self.get_world()
songs = muse_dash_world.included_songs.copy()
songs.append(muse_dash_world.victory_song_name)
self.assertIn("Operation Blade", songs, "Logical songs is missing a plando song: Operation Blade")
self.assertIn("Autumn Moods", songs, "Logical songs is missing a plando song: Autumn Moods")
self.assertIn("Fireflies", songs, "Logical songs is missing a plando song: Fireflies")
self.assertNotIn("Fireflies", songs, "Logical songs has added a filtered a plando song: Fireflies")
+33
View File
@@ -0,0 +1,33 @@
from . import MuseDashTestBase
class TestNoTraps(MuseDashTestBase):
def test_no_traps(self) -> None:
md_world = self.get_world()
md_world.options.chosen_traps.value.clear()
self.assertEqual(len(md_world.get_available_traps()), 0, "Got an available trap when we expected none.")
def test_all_traps(self) -> None:
md_world = self.get_world()
md_world.options.dlc_packs.value.add(md_world.md_collection.MUSE_PLUS_DLC)
for trap in md_world.md_collection.trap_items.keys():
md_world.options.chosen_traps.value.add(trap)
trap_count = len(md_world.get_available_traps())
true_count = len(md_world.md_collection.trap_items.keys())
self.assertEqual(trap_count, true_count, "Got a different amount of traps than what was expected.")
def test_exclude_sfx_traps(self) -> None:
md_world = self.get_world()
if "Muse Plus" in md_world.options.dlc_packs.value:
md_world.options.dlc_packs.value.remove("Muse Plus")
for trap in md_world.md_collection.trap_items.keys():
md_world.options.chosen_traps.value.add(trap)
trap_count = len(md_world.get_available_traps())
true_count = len(md_world.md_collection.trap_items.keys()) - len(md_world.md_collection.sfx_trap_items)
self.assertEqual(trap_count, true_count, "Got a different amount of traps than what was expected.")
@@ -4,30 +4,33 @@ from . import MuseDashTestBase
# This ends up with only 25 valid songs that can be chosen.
# These tests ensure that this won't fail generation
class TestWorstCaseHighDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
"allow_just_as_planned_dlc_songs": False,
"dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 11,
"song_difficulty_max": 11,
}
class TestWorstCaseMidDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
"allow_just_as_planned_dlc_songs": False,
"dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 6,
"song_difficulty_max": 6,
}
class TestWorstCaseLowDifficulty(MuseDashTestBase):
options = {
"starting_song_count": 10,
"allow_just_as_planned_dlc_songs": False,
"dlc_packs": [],
"streamer_mode_enabled": True,
"song_difficulty_mode": 6,
"song_difficulty_min": 1,
+6 -1
View File
@@ -1,5 +1,10 @@
from test.bases import WorldTestBase
from .. import MuseDashWorld
from typing import cast
class MuseDashTestBase(WorldTestBase):
game = "Muse Dash"
def get_world(self) -> MuseDashWorld:
return cast(MuseDashWorld, self.multiworld.worlds[1])
+1
View File
@@ -12,6 +12,7 @@ and won't show up in the wild. Previously they would be forced to show up exactl
- The Lilycove Wailmer now logically block you from the east. Actual game behavior is still unchanged for now.
- Water encounters in Slateport now correctly require Surf.
- Mirage Tower can no longer be your only logical access to a species in the wild, since it can permanently disappear.
- Updated the tracker link in the setup guide.
# 2.1.1
+9 -2
View File
@@ -25,13 +25,20 @@ IGNORABLE_MAPS = {
}
"""These maps exist but don't show up in the rando or are unused, and so should be discarded"""
POSTGAME_MAPS = {
OUT_OF_LOGIC_MAPS = {
"MAP_DESERT_UNDERPASS",
"MAP_SAFARI_ZONE_NORTHEAST",
"MAP_SAFARI_ZONE_SOUTHEAST",
"MAP_METEOR_FALLS_STEVENS_CAVE",
"MAP_MIRAGE_TOWER_1F",
"MAP_MIRAGE_TOWER_2F",
"MAP_MIRAGE_TOWER_3F",
"MAP_MIRAGE_TOWER_4F",
}
"""These maps have encounters and are locked behind beating the champion. Those encounter slots should be ignored for logical access to a species."""
"""
These maps have encounters and are locked behind beating the champion or are missable.
Those encounter slots should be ignored for logical access to a species.
"""
NUM_REAL_SPECIES = 386
+5 -5
View File
@@ -4,9 +4,8 @@ Functions related to pokemon species and moves
import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from Options import Toggle
from .data import NUM_REAL_SPECIES, POSTGAME_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, SpeciesData, data
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData,
SpeciesData, data)
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
@@ -266,7 +265,8 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
species_old_to_new_map: Dict[int, int] = {}
for species_id in table.slots:
if species_id not in species_old_to_new_map:
if not placed_priority_species and len(priority_species) > 0:
if not placed_priority_species and len(priority_species) > 0 \
and map_name not in OUT_OF_LOGIC_MAPS:
new_species_id = priority_species.pop()
placed_priority_species = True
else:
@@ -329,7 +329,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
new_species_id = world.random.choice(candidates).species_id
species_old_to_new_map[species_id] = new_species_id
if world.options.dexsanity and map_data.name not in POSTGAME_MAPS:
if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS:
already_placed.add(new_species_id)
# Actually create the new list of slots and encounter table
+4 -2
View File
@@ -7,7 +7,7 @@ from .ror2environments import environment_vanilla_table, environment_vanilla_ord
environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset
from BaseClasses import Item, ItemClassification, Tutorial
from .options import ItemWeights, ROR2Options
from .options import ItemWeights, ROR2Options, ror2_option_groups
from worlds.AutoWorld import World, WebWorld
from .regions import create_explore_regions, create_classic_regions
from typing import List, Dict, Any
@@ -23,6 +23,8 @@ class RiskOfWeb(WebWorld):
["Ijwu", "Kindasneaki"]
)]
option_groups = ror2_option_groups
class RiskOfRainWorld(World):
"""
@@ -44,7 +46,7 @@ class RiskOfRainWorld(World):
}
location_name_to_id = item_pickups
required_client_version = (0, 4, 5)
required_client_version = (0, 5, 0)
web = RiskOfWeb()
total_revivals: int
+43 -5
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions
from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions, OptionGroup
# NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks
@@ -350,7 +350,7 @@ class ItemPoolPresetToggle(Toggle):
class ItemWeights(Choice):
"""Set item_pool_presets to true if you want to use one of these presets.
"""Set Use Item Weight Presets to yes if you want to use one of these presets.
Preset choices for determining the weights of the item pool.
- New is a test for a potential adjustment to the default weights.
- Uncommon puts a large number of uncommon items in the pool.
@@ -375,6 +375,44 @@ class ItemWeights(Choice):
option_void = 9
ror2_option_groups = [
OptionGroup("Explore Mode Options", [
ChestsPerEnvironment,
ShrinesPerEnvironment,
ScavengersPerEnvironment,
ScannersPerEnvironment,
AltarsPerEnvironment,
RequireStages,
ProgressiveStages,
]),
OptionGroup("Classic Mode Options", [
TotalLocations,
], start_collapsed=True),
OptionGroup("Weighted Choices", [
ItemWeights,
ItemPoolPresetToggle,
WhiteScrap,
GreenScrap,
YellowScrap,
RedScrap,
CommonItem,
UncommonItem,
LegendaryItem,
BossItem,
LunarItem,
VoidItem,
Equipment,
Money,
LunarCoin,
Experience,
MountainTrap,
TimeWarpTrap,
CombatTrap,
TeleportTrap,
]),
]
@dataclass
class ROR2Options(PerGameCommonOptions):
goal: Goal
@@ -399,10 +437,10 @@ class ROR2Options(PerGameCommonOptions):
item_weights: ItemWeights
item_pool_presets: ItemPoolPresetToggle
# define the weights of the generated item pool.
green_scrap: GreenScrap
red_scrap: RedScrap
yellow_scrap: YellowScrap
white_scrap: WhiteScrap
green_scrap: GreenScrap
yellow_scrap: YellowScrap
red_scrap: RedScrap
common_item: CommonItem
uncommon_item: UncommonItem
legendary_item: LegendaryItem
+3 -1
View File
@@ -19,11 +19,13 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None:
# Default Locations
non_dlc_regions: Dict[str, RoRRegionData] = {
"Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)",
"Titanic Plains", "Titanic Plains (2)"]),
"Titanic Plains", "Titanic Plains (2)",
"Verdant Falls"]),
"Distant Roost": RoRRegionData([], ["OrderedStage_1"]),
"Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]),
"Titanic Plains": RoRRegionData([], ["OrderedStage_1"]),
"Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]),
"Verdant Falls": RoRRegionData([], ["OrderedStage_1"]),
"Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]),
"Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]),
"Rallypoint Delta": RoRRegionData([], ["OrderedStage_3"]),
+1
View File
@@ -7,6 +7,7 @@ environment_vanilla_orderedstage_1_table: Dict[str, int] = {
"Distant Roost (2)": 8, # blackbeach2
"Titanic Plains": 15, # golemplains
"Titanic Plains (2)": 16, # golemplains2
"Verdant Falls": 28, # lakes
}
environment_vanilla_orderedstage_2_table: Dict[str, int] = {
"Abandoned Aqueduct": 17, # goolake
+4 -5
View File
@@ -107,10 +107,10 @@ class ColouredMessage:
def coloured(self, text: str, colour: str) -> 'ColouredMessage':
add_json_text(self.parts, text, type="color", color=colour)
return self
def location(self, location_id: int, player_id: int = 0) -> 'ColouredMessage':
def location(self, location_id: int, player_id: int) -> 'ColouredMessage':
add_json_location(self.parts, location_id, player_id)
return self
def item(self, item_id: int, player_id: int = 0, flags: int = 0) -> 'ColouredMessage':
def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage':
add_json_item(self.parts, item_id, player_id, flags)
return self
def player(self, player_id: int) -> 'ColouredMessage':
@@ -122,7 +122,6 @@ class ColouredMessage:
class StarcraftClientProcessor(ClientCommandProcessor):
ctx: SC2Context
echo_commands = True
def formatted_print(self, text: str) -> None:
"""Prints with kivy formatting to the GUI, and also prints to command-line and to all logs"""
@@ -257,7 +256,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
for item in received_items_of_this_type:
print_faction_title()
has_printed_faction_title = True
(ColouredMessage('* ').item(item.item, flags=item.flags)
(ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player)
).send(self.ctx)
@@ -278,7 +277,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
received_items_of_this_type = items_received.get(child_item, [])
for item in received_items_of_this_type:
filter_match_count += len(received_items_of_this_type)
(ColouredMessage(' * ').item(item.item, flags=item.flags)
(ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags)
(" from ").location(item.location, self.ctx.slot)
(" by ").player(item.player)
).send(self.ctx)
+7 -7
View File
@@ -10,15 +10,15 @@ class ItemDict(TypedDict):
base_id = 82000
item_table: List[ItemDict] = [
{"name": "Stick", "id": base_id + 1, "count": 8, "classification": ItemClassification.progression_skip_balancing},
{"name": "Stick", "id": base_id + 1, "count": 0, "classification": ItemClassification.progression_skip_balancing},
{"name": "Seashell", "id": base_id + 2, "count": 23, "classification": ItemClassification.progression_skip_balancing},
{"name": "Golden Feather", "id": base_id + 3, "count": 0, "classification": ItemClassification.progression},
{"name": "Silver Feather", "id": base_id + 4, "count": 0, "classification": ItemClassification.useful},
{"name": "Bucket", "id": base_id + 5, "count": 0, "classification": ItemClassification.progression},
{"name": "Bait", "id": base_id + 6, "count": 2, "classification": ItemClassification.filler},
{"name": "Fishing Rod", "id": base_id + 7, "count": 2, "classification": ItemClassification.progression},
{"name": "Progressive Fishing Rod", "id": base_id + 7, "count": 2, "classification": ItemClassification.progression},
{"name": "Shovel", "id": base_id + 8, "count": 1, "classification": ItemClassification.progression},
{"name": "Toy Shovel", "id": base_id + 9, "count": 5, "classification": ItemClassification.progression_skip_balancing},
{"name": "Toy Shovel", "id": base_id + 9, "count": 0, "classification": ItemClassification.progression_skip_balancing},
{"name": "Compass", "id": base_id + 10, "count": 1, "classification": ItemClassification.useful},
{"name": "Medal", "id": base_id + 11, "count": 3, "classification": ItemClassification.filler},
{"name": "Shell Necklace", "id": base_id + 12, "count": 1, "classification": ItemClassification.progression},
@@ -36,7 +36,7 @@ item_table: List[ItemDict] = [
{"name": "Headband", "id": base_id + 24, "count": 1, "classification": ItemClassification.progression},
{"name": "Running Shoes", "id": base_id + 25, "count": 1, "classification": ItemClassification.useful},
{"name": "Camping Permit", "id": base_id + 26, "count": 1, "classification": ItemClassification.progression},
{"name": "Walkie Talkie", "id": base_id + 27, "count": 1, "classification": ItemClassification.useful},
{"name": "Walkie Talkie", "id": base_id + 27, "count": 0, "classification": ItemClassification.useful},
# Not in the item pool for now
#{"name": "Boating Manual", "id": base_id + ~, "count": 1, "classification": ItemClassification.filler},
@@ -48,9 +48,9 @@ item_table: List[ItemDict] = [
{"name": "21 Coins", "id": base_id + 31, "count": 2, "classification": ItemClassification.filler},
{"name": "25 Coins", "id": base_id + 32, "count": 7, "classification": ItemClassification.filler},
{"name": "27 Coins", "id": base_id + 33, "count": 1, "classification": ItemClassification.filler},
{"name": "32 Coins", "id": base_id + 34, "count": 1, "classification": ItemClassification.filler},
{"name": "33 Coins", "id": base_id + 35, "count": 6, "classification": ItemClassification.filler},
{"name": "50 Coins", "id": base_id + 36, "count": 1, "classification": ItemClassification.filler},
{"name": "32 Coins", "id": base_id + 34, "count": 1, "classification": ItemClassification.useful},
{"name": "33 Coins", "id": base_id + 35, "count": 6, "classification": ItemClassification.useful},
{"name": "50 Coins", "id": base_id + 36, "count": 1, "classification": ItemClassification.useful},
# Filler item determined by settings
{"name": "13 Coins", "id": base_id + 37, "count": 0, "classification": ItemClassification.filler},
+132 -132
View File
@@ -5,7 +5,7 @@ class LocationInfo(TypedDict):
id: int
inGameId: str
needsShovel: bool
purchase: bool
purchase: int
minGoldenFeathers: int
minGoldenFeathersEasy: int
minGoldenFeathersBucket: int
@@ -17,311 +17,311 @@ location_table: List[LocationInfo] = [
{"name": "Start Beach Seashell",
"id": base_id + 1,
"inGameId": "PickUps.3",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Beach Hut Seashell",
"id": base_id + 2,
"inGameId": "PickUps.2",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Beach Umbrella Seashell",
"id": base_id + 3,
"inGameId": "PickUps.8",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sid Beach Mound Seashell",
"id": base_id + 4,
"inGameId": "PickUps.12",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sid Beach Seashell",
"id": base_id + 5,
"inGameId": "PickUps.11",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Shirley's Point Beach Seashell",
"id": base_id + 6,
"inGameId": "PickUps.18",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Shirley's Point Rock Seashell",
"id": base_id + 7,
"inGameId": "PickUps.17",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Beach Seashell",
"id": base_id + 8,
"inGameId": "PickUps.19",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "West River Seashell",
"id": base_id + 9,
"inGameId": "PickUps.10",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "West Riverbank Seashell",
"id": base_id + 10,
"inGameId": "PickUps.4",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Stone Tower Riverbank Seashell",
"id": base_id + 11,
"inGameId": "PickUps.23",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "North Beach Seashell",
"id": base_id + 12,
"inGameId": "PickUps.6",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "North Coast Seashell",
"id": base_id + 13,
"inGameId": "PickUps.7",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Boat Cliff Seashell",
"id": base_id + 14,
"inGameId": "PickUps.14",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Boat Isle Mound Seashell",
"id": base_id + 15,
"inGameId": "PickUps.22",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "East Coast Seashell",
"id": base_id + 16,
"inGameId": "PickUps.21",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "House North Beach Seashell",
"id": base_id + 17,
"inGameId": "PickUps.16",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Airstream Island North Seashell",
"id": base_id + 18,
"inGameId": "PickUps.13",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Airstream Island South Seashell",
"id": base_id + 19,
"inGameId": "PickUps.15",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Secret Island Beach Seashell",
"id": base_id + 20,
"inGameId": "PickUps.1",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Meteor Lake Seashell",
"id": base_id + 126,
"inGameId": "PickUps.20",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Good Creek Path Seashell",
"id": base_id + 127,
"inGameId": "PickUps.9",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
# Visitor's Center Shop
{"name": "Visitor's Center Shop Golden Feather 1",
"id": base_id + 21,
"inGameId": "CampRangerNPC[0]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 40,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Shop Golden Feather 2",
"id": base_id + 22,
"inGameId": "CampRangerNPC[1]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 40,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Shop Hat",
"id": base_id + 23,
"inGameId": "CampRangerNPC[9]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Tough Bird Salesman
{"name": "Tough Bird Salesman Golden Feather 1",
"id": base_id + 24,
"inGameId": "ToughBirdNPC (1)[0]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Tough Bird Salesman Golden Feather 2",
"id": base_id + 25,
"inGameId": "ToughBirdNPC (1)[1]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Tough Bird Salesman Golden Feather 3",
"id": base_id + 26,
"inGameId": "ToughBirdNPC (1)[2]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Tough Bird Salesman Golden Feather 4",
"id": base_id + 27,
"inGameId": "ToughBirdNPC (1)[3]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Tough Bird Salesman (400 Coins)",
"id": base_id + 28,
"inGameId": "ToughBirdNPC (1)[9]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 400,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
# Beachstickball
{"name": "Beachstickball (10 Hits)",
"id": base_id + 29,
"inGameId": "VolleyballOpponent[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Beachstickball (20 Hits)",
"id": base_id + 30,
"inGameId": "VolleyballOpponent[1]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Beachstickball (30 Hits)",
"id": base_id + 31,
"inGameId": "VolleyballOpponent[2]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Misc Item Locations
{"name": "Shovel Kid Trade",
"id": base_id + 32,
"inGameId": "Frog_StandingNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Compass Guy",
"id": base_id + 33,
"inGameId": "Fox_WalkingNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Hawk Peak Bucket Rock",
"id": base_id + 34,
"inGameId": "Tools.23",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands Bucket Rock",
"id": base_id + 35,
"inGameId": "Tools.42",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Bill the Walrus Fisherman",
"id": base_id + 36,
"inGameId": "SittingNPC (1)[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Catch 3 Fish Reward",
"id": base_id + 37,
"inGameId": "FishBuyer[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Catch All Fish Reward",
"id": base_id + 38,
"inGameId": "FishBuyer[1]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7},
{"name": "Permit Guy Bribe",
"id": base_id + 39,
"inGameId": "CamperNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Catch Fish with Permit",
"id": base_id + 129,
"inGameId": "Player[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Return Camping Permit",
"id": base_id + 130,
"inGameId": "CamperNPC[1]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
# Original Pickaxe Locations
{"name": "Blocked Mine Pickaxe 1",
"id": base_id + 40,
"inGameId": "Tools.31",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Blocked Mine Pickaxe 2",
"id": base_id + 41,
"inGameId": "Tools.32",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Blocked Mine Pickaxe 3",
"id": base_id + 42,
"inGameId": "Tools.33",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Original Toy Shovel Locations
{"name": "Blackwood Trail Lookout Toy Shovel",
"id": base_id + 43,
"inGameId": "PickUps.27",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Shirley's Point Beach Toy Shovel",
"id": base_id + 44,
"inGameId": "PickUps.30",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Beach Toy Shovel",
"id": base_id + 45,
"inGameId": "PickUps.29",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Blackwood Trail Rock Toy Shovel",
"id": base_id + 46,
"inGameId": "PickUps.26",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Beach Hut Cliff Toy Shovel",
"id": base_id + 128,
"inGameId": "PickUps.28",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Original Stick Locations
{"name": "Secret Island Beach Trail Stick",
"id": base_id + 47,
"inGameId": "PickUps.25",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Below Lighthouse Walkway Stick",
"id": base_id + 48,
"inGameId": "Tools.3",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Beach Hut Rocky Pool Sand Stick",
"id": base_id + 49,
"inGameId": "Tools.0",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Cliff Overlooking West River Waterfall Stick",
"id": base_id + 50,
"inGameId": "Tools.2",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0},
{"name": "Trail to Tough Bird Salesman Stick",
"id": base_id + 51,
"inGameId": "Tools.8",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "North Beach Stick",
"id": base_id + 52,
"inGameId": "Tools.4",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Beachstickball Court Stick",
"id": base_id + 53,
"inGameId": "VolleyballMinigame.4",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Stick Under Sid Beach Umbrella",
"id": base_id + 54,
"inGameId": "Tools.1",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Boating
@@ -333,377 +333,377 @@ location_table: List[LocationInfo] = [
{"name": "Boat Challenge Reward",
"id": base_id + 56,
"inGameId": "DeerKidBoat[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Not a location for now, corresponding with the Boating Manual
# {"name": "Receive Boating Manual",
# "id": base_id + 133,
# "inGameId": "DadDeer[1]",
# "needsShovel": False, "purchase": False,
# "needsShovel": False, "purchase": 0,
# "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Original Map Locations
{"name": "Outlook Point Dog Gift",
"id": base_id + 57,
"inGameId": "Dog_WalkingNPC_BlueEyed[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
# Original Clothes Locations
{"name": "Collect 15 Seashells",
"id": base_id + 58,
"inGameId": "LittleKidNPCVariant (1)[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Return to Shell Kid",
"id": base_id + 132,
"inGameId": "LittleKidNPCVariant (1)[1]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Taylor the Turtle Headband Gift",
"id": base_id + 59,
"inGameId": "Turtle_WalkingNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Sue the Rabbit Shoes Reward",
"id": base_id + 60,
"inGameId": "Bunny_WalkingNPC (1)[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Purchase Sunhat",
"id": base_id + 61,
"inGameId": "SittingNPC[0]",
"needsShovel": False, "purchase": True,
"needsShovel": False, "purchase": 100,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Original Golden Feather Locations
{"name": "Blackwood Forest Golden Feather",
"id": base_id + 62,
"inGameId": "Feathers.3",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Ranger May Shell Necklace Golden Feather",
"id": base_id + 63,
"inGameId": "AuntMayNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sand Castle Golden Feather",
"id": base_id + 64,
"inGameId": "SandProvince.3",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Artist Golden Feather",
"id": base_id + 65,
"inGameId": "StandingNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Visitor Camp Rock Golden Feather",
"id": base_id + 66,
"inGameId": "Feathers.8",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Outlook Cliff Golden Feather",
"id": base_id + 67,
"inGameId": "Feathers.2",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Meteor Lake Cliff Golden Feather",
"id": base_id + 68,
"inGameId": "Feathers.7",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0},
# Original Silver Feather Locations
{"name": "Secret Island Peak",
"id": base_id + 69,
"inGameId": "PickUps.24",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 5, "minGoldenFeathersEasy": 7, "minGoldenFeathersBucket": 7},
{"name": "Wristwatch Trade",
"id": base_id + 70,
"inGameId": "Goat_StandingNPC[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
# Golden Chests
{"name": "Lighthouse Golden Chest",
"id": base_id + 71,
"inGameId": "Feathers.0",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 0},
{"name": "Outlook Golden Chest",
"id": base_id + 72,
"inGameId": "Feathers.6",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Stone Tower Golden Chest",
"id": base_id + 73,
"inGameId": "Feathers.5",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "North Cliff Golden Chest",
"id": base_id + 74,
"inGameId": "Feathers.4",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 3, "minGoldenFeathersEasy": 10, "minGoldenFeathersBucket": 10},
# Chests
{"name": "Blackwood Cliff Chest",
"id": base_id + 75,
"inGameId": "Coins.22",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "White Coast Trail Chest",
"id": base_id + 76,
"inGameId": "Coins.6",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sid Beach Chest",
"id": base_id + 77,
"inGameId": "Coins.7",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sid Beach Buried Treasure Chest",
"id": base_id + 78,
"inGameId": "Coins.46",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Sid Beach Cliff Chest",
"id": base_id + 79,
"inGameId": "Coins.9",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Buried Chest",
"id": base_id + 80,
"inGameId": "Coins.94",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Visitor's Center Hidden Chest",
"id": base_id + 81,
"inGameId": "Coins.42",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Shirley's Point Chest",
"id": base_id + 82,
"inGameId": "Coins.10",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 1, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 2},
{"name": "Caravan Cliff Chest",
"id": base_id + 83,
"inGameId": "Coins.12",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Caravan Arch Chest",
"id": base_id + 84,
"inGameId": "Coins.11",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "King Buried Treasure Chest",
"id": base_id + 85,
"inGameId": "Coins.41",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Good Creek Path Buried Chest",
"id": base_id + 86,
"inGameId": "Coins.48",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Good Creek Path West Chest",
"id": base_id + 87,
"inGameId": "Coins.33",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Good Creek Path East Chest",
"id": base_id + 88,
"inGameId": "Coins.62",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "West Waterfall Chest",
"id": base_id + 89,
"inGameId": "Coins.20",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Stone Tower West Cliff Chest",
"id": base_id + 90,
"inGameId": "PickUps.0",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Bucket Path Chest",
"id": base_id + 91,
"inGameId": "Coins.50",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Bucket Cliff Chest",
"id": base_id + 92,
"inGameId": "Coins.49",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
{"name": "In Her Shadow Buried Treasure Chest",
"id": base_id + 93,
"inGameId": "Feathers.9",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Meteor Lake Buried Chest",
"id": base_id + 94,
"inGameId": "Coins.86",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Meteor Lake Chest",
"id": base_id + 95,
"inGameId": "Coins.64",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "House North Beach Chest",
"id": base_id + 96,
"inGameId": "Coins.65",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "East Coast Chest",
"id": base_id + 97,
"inGameId": "Coins.98",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Fisherman's Boat Chest 1",
"id": base_id + 99,
"inGameId": "Boat.0",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Fisherman's Boat Chest 2",
"id": base_id + 100,
"inGameId": "Boat.7",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Airstream Island Chest",
"id": base_id + 101,
"inGameId": "Coins.31",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "West River Waterfall Head Chest",
"id": base_id + 102,
"inGameId": "Coins.34",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Old Building Chest",
"id": base_id + 103,
"inGameId": "Coins.104",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Old Building West Chest",
"id": base_id + 104,
"inGameId": "Coins.109",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Old Building East Chest",
"id": base_id + 105,
"inGameId": "Coins.8",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Hawk Peak West Chest",
"id": base_id + 106,
"inGameId": "Coins.21",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
{"name": "Hawk Peak East Buried Chest",
"id": base_id + 107,
"inGameId": "Coins.76",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
{"name": "Hawk Peak Northeast Chest",
"id": base_id + 108,
"inGameId": "Coins.79",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5},
{"name": "Northern East Coast Chest",
"id": base_id + 109,
"inGameId": "Coins.45",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0},
{"name": "North Coast Chest",
"id": base_id + 110,
"inGameId": "Coins.28",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "North Coast Buried Chest",
"id": base_id + 111,
"inGameId": "Coins.47",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Small South Island Buried Chest",
"id": base_id + 112,
"inGameId": "Coins.87",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Secret Island Bottom Chest",
"id": base_id + 113,
"inGameId": "Coins.88",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Secret Island Treehouse Chest",
"id": base_id + 114,
"inGameId": "Coins.89",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 1},
{"name": "Sunhat Island Buried Chest",
"id": base_id + 115,
"inGameId": "Coins.112",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands South Buried Chest",
"id": base_id + 116,
"inGameId": "Coins.119",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands West Chest",
"id": base_id + 117,
"inGameId": "Coins.121",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands North Buried Chest",
"id": base_id + 118,
"inGameId": "Coins.117",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands East Chest",
"id": base_id + 119,
"inGameId": "Coins.120",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands South Hidden Chest",
"id": base_id + 120,
"inGameId": "Coins.124",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "A Stormy View Buried Treasure Chest",
"id": base_id + 121,
"inGameId": "Coins.113",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
{"name": "Orange Islands Ruins Buried Chest",
"id": base_id + 122,
"inGameId": "Coins.118",
"needsShovel": True, "purchase": False,
"needsShovel": True, "purchase": 0,
"minGoldenFeathers": 2, "minGoldenFeathersEasy": 4, "minGoldenFeathersBucket": 0},
# Race Rewards
{"name": "Lighthouse Race Reward",
"id": base_id + 123,
"inGameId": "RaceOpponent[0]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 1},
{"name": "Old Building Race Reward",
"id": base_id + 124,
"inGameId": "RaceOpponent[1]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 1, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0},
{"name": "Hawk Peak Race Reward",
"id": base_id + 125,
"inGameId": "RaceOpponent[2]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7},
{"name": "Lose Race Gift",
"id": base_id + 131,
"inGameId": "RaceOpponent[9]",
"needsShovel": False, "purchase": False,
"needsShovel": False, "purchase": 0,
"minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0},
]
+84 -3
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from Options import Choice, PerGameCommonOptions, Range, StartInventoryPool, Toggle
from Options import Choice, OptionGroup, PerGameCommonOptions, Range, StartInventoryPool, Toggle, DefaultOnToggle
class Goal(Choice):
"""Choose the end goal.
@@ -22,8 +22,10 @@ class CoinsInShops(Toggle):
default = False
class GoldenFeathers(Range):
"""Number of Golden Feathers in the item pool.
(Note that for the Photo and Help Everyone goals, a minimum of 12 Golden Feathers is enforced)"""
"""
Number of Golden Feathers in the item pool.
(Note that for the Photo and Help Everyone goals, a minimum of 12 Golden Feathers is enforced)
"""
display_name = "Golden Feathers"
range_start = 0
range_end = 20
@@ -43,6 +45,20 @@ class Buckets(Range):
range_end = 2
default = 2
class Sticks(Range):
"""Number of Sticks in the item pool."""
display_name = "Sticks"
range_start = 1
range_end = 8
default = 8
class ToyShovels(Range):
"""Number of Toy Shovels in the item pool."""
display_name = "Toy Shovels"
range_start = 1
range_end = 5
default = 5
class GoldenFeatherProgression(Choice):
"""Determines which locations are considered in logic based on the required amount of golden feathers to reach them.
Easy: Locations will be considered inaccessible until the player has enough golden feathers to easily reach them. A minimum of 10 golden feathers is recommended for this setting.
@@ -76,6 +92,40 @@ class FillerCoinAmount(Choice):
option_50_coins = 9
default = 1
class RandomWalkieTalkie(DefaultOnToggle):
"""
When enabled, the Walkie Talkie item will be placed into the item pool. Otherwise, it will be placed in its vanilla location.
This item usually allows the player to locate Avery around the map or restart a race.
"""
display_name = "Randomize Walkie Talkie"
class EasierRaces(Toggle):
"""When enabled, the Running Shoes will be added as a logical requirement for beating any of the races."""
display_name = "Easier Races"
class ShopCheckLogic(Choice):
"""Determines which items will be added as logical requirements to making certain purchases in shops."""
display_name = "Shop Check Logic"
option_nothing = 0
option_fishing_rod = 1
option_shovel = 2
option_fishing_rod_and_shovel = 3
option_golden_fishing_rod = 4
option_golden_fishing_rod_and_shovel = 5
default = 1
class MinShopCheckLogic(Choice):
"""
Determines the minimum cost of a shop item that will have the shop check logic applied to it.
If the cost of a shop item is less than this value, no items will be required to access it.
This is based on the vanilla prices of the shop item. The set cost multiplier will not affect this value.
"""
display_name = "Minimum Shop Check Logic Application"
option_40_coins = 0
option_100_coins = 1
option_400_coins = 2
default = 1
@dataclass
class ShortHikeOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
@@ -84,6 +134,37 @@ class ShortHikeOptions(PerGameCommonOptions):
golden_feathers: GoldenFeathers
silver_feathers: SilverFeathers
buckets: Buckets
sticks: Sticks
toy_shovels: ToyShovels
golden_feather_progression: GoldenFeatherProgression
cost_multiplier: CostMultiplier
filler_coin_amount: FillerCoinAmount
random_walkie_talkie: RandomWalkieTalkie
easier_races: EasierRaces
shop_check_logic: ShopCheckLogic
min_shop_check_logic: MinShopCheckLogic
shorthike_option_groups = [
OptionGroup("General Options", [
Goal,
FillerCoinAmount,
RandomWalkieTalkie
]),
OptionGroup("Logic Options", [
GoldenFeatherProgression,
EasierRaces
]),
OptionGroup("Item Pool Options", [
GoldenFeathers,
SilverFeathers,
Buckets,
Sticks,
ToyShovels
]),
OptionGroup("Shop Options", [
CoinsInShops,
CostMultiplier,
ShopCheckLogic,
MinShopCheckLogic
])
]
+39 -6
View File
@@ -1,4 +1,6 @@
from worlds.generic.Rules import forbid_items_for_player, add_rule
from .Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic
def create_rules(self, location_table):
multiworld = self.multiworld
@@ -11,11 +13,23 @@ def create_rules(self, location_table):
forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Maps'], player)
add_rule(multiworld.get_location(loc["name"], player),
lambda state: state.has("Shovel", player))
# Shop Rules
if loc["purchase"] and not options.coins_in_shops:
forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Coins'], player)
if loc["purchase"] >= get_min_shop_logic_cost(self) and options.shop_check_logic != ShopCheckLogic.option_nothing:
if options.shop_check_logic in {ShopCheckLogic.option_fishing_rod, ShopCheckLogic.option_fishing_rod_and_shovel}:
add_rule(multiworld.get_location(loc["name"], player),
lambda state: state.has("Progressive Fishing Rod", player))
if options.shop_check_logic in {ShopCheckLogic.option_golden_fishing_rod, ShopCheckLogic.option_golden_fishing_rod_and_shovel}:
add_rule(multiworld.get_location(loc["name"], player),
lambda state: state.has("Progressive Fishing Rod", player, 2))
if options.shop_check_logic in {ShopCheckLogic.option_shovel, ShopCheckLogic.option_fishing_rod_and_shovel, ShopCheckLogic.option_golden_fishing_rod_and_shovel}:
add_rule(multiworld.get_location(loc["name"], player),
lambda state: state.has("Shovel", player))
# Minimum Feather Rules
if options.golden_feather_progression != 2:
if options.golden_feather_progression != GoldenFeatherProgression.option_hard:
min_feathers = get_min_feathers(self, loc["minGoldenFeathers"], loc["minGoldenFeathersEasy"])
if options.buckets > 0 and loc["minGoldenFeathersBucket"] < min_feathers:
@@ -32,11 +46,11 @@ def create_rules(self, location_table):
# Fishing Rules
add_rule(multiworld.get_location("Catch 3 Fish Reward", player),
lambda state: state.has("Fishing Rod", player))
lambda state: state.has("Progressive Fishing Rod", player))
add_rule(multiworld.get_location("Catch Fish with Permit", player),
lambda state: state.has("Fishing Rod", player))
lambda state: state.has("Progressive Fishing Rod", player))
add_rule(multiworld.get_location("Catch All Fish Reward", player),
lambda state: state.has("Fishing Rod", player))
lambda state: state.has("Progressive Fishing Rod", player, 2))
# Misc Rules
add_rule(multiworld.get_location("Return Camping Permit", player),
@@ -59,15 +73,34 @@ def create_rules(self, location_table):
lambda state: state.has("Stick", player))
add_rule(multiworld.get_location("Beachstickball (30 Hits)", player),
lambda state: state.has("Stick", player))
# Race Rules
if options.easier_races:
add_rule(multiworld.get_location("Lighthouse Race Reward", player),
lambda state: state.has("Running Shoes", player))
add_rule(multiworld.get_location("Old Building Race Reward", player),
lambda state: state.has("Running Shoes", player))
add_rule(multiworld.get_location("Hawk Peak Race Reward", player),
lambda state: state.has("Running Shoes", player))
def get_min_feathers(self, min_golden_feathers, min_golden_feathers_easy):
options = self.options
min_feathers = min_golden_feathers
if options.golden_feather_progression == 0:
if options.golden_feather_progression == GoldenFeatherProgression.option_easy:
min_feathers = min_golden_feathers_easy
if min_feathers > options.golden_feathers:
if options.goal != 1 and options.goal != 3:
if options.goal not in {Goal.option_help_everyone, Goal.option_photo}:
min_feathers = options.golden_feathers
return min_feathers
def get_min_shop_logic_cost(self):
options = self.options
if options.min_shop_check_logic == MinShopCheckLogic.option_40_coins:
return 40
elif options.min_shop_check_logic == MinShopCheckLogic.option_100_coins:
return 100
elif options.min_shop_check_logic == MinShopCheckLogic.option_400_coins:
return 400
+41 -20
View File
@@ -1,12 +1,11 @@
from collections import Counter
from typing import ClassVar, Dict, Any, Type
from BaseClasses import Region, Location, Item, Tutorial
from BaseClasses import ItemClassification, Region, Location, Item, Tutorial
from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld
from .Items import item_table, group_table, base_id
from .Locations import location_table
from .Rules import create_rules, get_min_feathers
from .Options import ShortHikeOptions
from .Options import ShortHikeOptions, shorthike_option_groups
class ShortHikeWeb(WebWorld):
theme = "ocean"
@@ -18,6 +17,7 @@ class ShortHikeWeb(WebWorld):
"setup/en",
["Chandler"]
)]
option_groups = shorthike_option_groups
class ShortHikeWorld(World):
"""
@@ -47,9 +47,14 @@ class ShortHikeWorld(World):
item_id: int = self.item_name_to_id[name]
id = item_id - base_id - 1
return ShortHikeItem(name, item_table[id]["classification"], item_id, player=self.player)
classification = item_table[id]["classification"]
if self.options.easier_races and name == "Running Shoes":
classification = ItemClassification.progression
return ShortHikeItem(name, classification, item_id, player=self.player)
def create_items(self) -> None:
itempool = []
for item in item_table:
count = item["count"]
@@ -57,18 +62,28 @@ class ShortHikeWorld(World):
continue
else:
for i in range(count):
self.multiworld.itempool.append(self.create_item(item["name"]))
itempool.append(self.create_item(item["name"]))
feather_count = self.options.golden_feathers
if self.options.goal == 1 or self.options.goal == 3:
if feather_count < 12:
feather_count = 12
junk = 45 - self.options.silver_feathers - feather_count - self.options.buckets
self.multiworld.itempool += [self.create_item(self.get_filler_item_name()) for _ in range(junk)]
self.multiworld.itempool += [self.create_item("Golden Feather") for _ in range(feather_count)]
self.multiworld.itempool += [self.create_item("Silver Feather") for _ in range(self.options.silver_feathers)]
self.multiworld.itempool += [self.create_item("Bucket") for _ in range(self.options.buckets)]
itempool += [self.create_item("Golden Feather") for _ in range(feather_count)]
itempool += [self.create_item("Silver Feather") for _ in range(self.options.silver_feathers)]
itempool += [self.create_item("Bucket") for _ in range(self.options.buckets)]
itempool += [self.create_item("Stick") for _ in range(self.options.sticks)]
itempool += [self.create_item("Toy Shovel") for _ in range(self.options.toy_shovels)]
if self.options.random_walkie_talkie:
itempool.append(self.create_item("Walkie Talkie"))
else:
self.multiworld.get_location("Lose Race Gift", self.player).place_locked_item(self.create_item("Walkie Talkie"))
junk = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
itempool += [self.create_item(self.get_filler_item_name()) for _ in range(junk)]
self.multiworld.itempool += itempool
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
@@ -92,20 +107,23 @@ class ShortHikeWorld(World):
self.multiworld.completion_condition[self.player] = lambda state: state.has("Golden Feather", self.player, 12)
elif self.options.goal == "races":
# Races
self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9))
or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7)))
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Hawk Peak Race Reward", self.player)
elif self.options.goal == "help_everyone":
# Help Everyone
self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, 12)
and state.has("Toy Shovel", self.player) and state.has("Camping Permit", self.player)
and state.has("Motorboat Key", self.player) and state.has("Headband", self.player)
and state.has("Wristwatch", self.player) and state.has("Seashell", self.player, 15)
and state.has("Shell Necklace", self.player))
self.multiworld.completion_condition[self.player] = lambda state: (state.can_reach_location("Collect 15 Seashells", self.player)
and state.has("Golden Feather", self.player, 12)
and state.can_reach_location("Tough Bird Salesman (400 Coins)", self.player)
and state.can_reach_location("Ranger May Shell Necklace Golden Feather", self.player)
and state.can_reach_location("Sue the Rabbit Shoes Reward", self.player)
and state.can_reach_location("Wristwatch Trade", self.player)
and state.can_reach_location("Return Camping Permit", self.player)
and state.can_reach_location("Boat Challenge Reward", self.player)
and state.can_reach_location("Shovel Kid Trade", self.player)
and state.can_reach_location("Purchase Sunhat", self.player)
and state.can_reach_location("Artist Golden Feather", self.player))
elif self.options.goal == "fishmonger":
# Fishmonger
self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9))
or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7))
and state.has("Fishing Rod", self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Catch All Fish Reward", self.player)
def set_rules(self):
create_rules(self, location_table)
@@ -117,6 +135,9 @@ class ShortHikeWorld(World):
"goal": int(options.goal),
"logicLevel": int(options.golden_feather_progression),
"costMultiplier": int(options.cost_multiplier),
"shopCheckLogic": int(options.shop_check_logic),
"minShopCheckLogic": int(options.min_shop_check_logic),
"easierRaces": bool(options.easier_races),
}
slot_data = {
+3 -9
View File
@@ -4,7 +4,6 @@
- A Short Hike: [Steam](https://store.steampowered.com/app/1055540/A_Short_Hike/)
- The Epic Games Store or itch.io version of A Short Hike will also work.
- A Short Hike Modding Tools: [GitHub](https://github.com/BrandenEK/AShortHike.ModdingTools)
- A Short Hike Randomizer: [GitHub](https://github.com/BrandenEK/AShortHike.Randomizer)
## Optional Software
@@ -14,18 +13,13 @@
## Installation
1. Open the [Modding Tools GitHub page](https://github.com/BrandenEK/AShortHike.ModdingTools/), and follow
the installation instructions. After this step, your `A Short Hike/` folder should have an empty `Modding/` subfolder.
2. After the Modding Tools have been installed, download the
[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) zip, extract it, and move the contents
of the `Randomizer/` folder into your `Modding/` folder. After this step, your `Modding/` folder should have
`data/` and `plugins/` subfolders.
Open the [Randomizer Repository](https://github.com/BrandenEK/AShortHike.Randomizer) and follow
the installation instructions listed there.
## Connecting
A Short Hike will prompt you with the server details when a new game is started or a previous one is continued.
Enter in the Server Port, Name, and Password (optional) in the popup menu that appears and hit connect.
Enter in the Server Address and Port, Name, and Password (optional) in the popup menu that appears and hit connect.
## Tracking
+13 -5
View File
@@ -70,7 +70,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
logic = TimespinnerLogic(world, player, precalculated_weights)
connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state))
connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Upper lake desolation', 'Lake desolation')
@@ -80,7 +80,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Eastern lake desolation', 'Library')
connect(world, player, 'Eastern lake desolation', 'Lower lake desolation')
connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player))
connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene")
connect(world, player, 'Library', 'Eastern lake desolation')
connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player))
connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D)
@@ -185,7 +185,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w
if is_option_enabled(world, player, "GyreArchives"):
connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player))
connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)')
connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player))
connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp")
connect(world, player, 'Ifrit\'s Lair', 'Library top')
@@ -242,11 +242,19 @@ def connectStartingRegion(world: MultiWorld, player: int):
def connect(world: MultiWorld, player: int, source: str, target: str,
rule: Optional[Callable[[CollectionState], bool]] = None):
rule: Optional[Callable[[CollectionState], bool]] = None,
indirect: str = ""):
sourceRegion = world.get_region(source, player)
targetRegion = world.get_region(target, player)
sourceRegion.connect(targetRegion, rule=rule)
entrance = sourceRegion.connect(targetRegion, rule=rule)
if indirect:
indirectRegion = world.get_region(indirect, player)
if indirectRegion in world.indirect_connections:
world.indirect_connections[indirectRegion].add(entrance)
else:
world.indirect_connections[indirectRegion] = {entrance}
def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]:
+4 -3
View File
@@ -3,11 +3,12 @@
## Required Software
- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported)
- [BepInEx (Unity IL2CPP)](https://github.com/BepInEx/BepInEx/releases/tag/v6.0.0-pre.1)
- [TUNIC Randomizer Mod](https://github.com/silent-destroyer/tunic-randomizer/releases/latest)
- [BepInEx 6.0.0-pre.1 (Unity IL2CPP x64)](https://github.com/BepInEx/BepInEx/releases/tag/v6.0.0-pre.1)
## Optional Software
- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest) (For use with EmoTracker/PopTracker)
- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest)
- Requires [PopTracker](https://github.com/black-sliver/PopTracker/releases)
- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest)
- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
@@ -27,7 +28,7 @@ Find your TUNIC game installation directory:
BepInEx is a general purpose framework for modding Unity games, and is used to run the TUNIC Randomizer.
Download [BepInEx](https://github.com/BepInEx/BepInEx/releases/download/v6.0.0-pre.1/BepInEx_UnityIL2CPP_x64_6.0.0-pre.1.zip).
Download [BepInEx 6.0.0-pre.1 (Unity IL2CPP x64)](https://github.com/BepInEx/BepInEx/releases/tag/v6.0.0-pre.1).
If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html).
-2
View File
@@ -1462,8 +1462,6 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int])
# Quarry
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player),
lambda state: has_mask(state, player, options))
# Ziggurat
set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player),
+3 -3
View File
@@ -304,15 +304,15 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) ->
# Quarry
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player),
lambda state: has_mask(state, player, options))
# nmg - kill boss scav with orb + firecracker, or similar
set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player),
lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules))
# Swamp
set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player),
lambda state: state.has(laurels, player) and state.has(fire_wand, player) and has_sword(state, player))
lambda state: (state.has(fire_wand, player) and has_sword(state, player))
and (state.has(laurels, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)))
set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),
+5 -4
View File
@@ -1,7 +1,8 @@
from collections import defaultdict
from functools import lru_cache
from typing import Dict, List, Set, Tuple
from Utils import cache_argsless
from .item_definition_classes import (
CATEGORY_NAME_MAPPINGS,
DoorItemDefinition,
@@ -260,17 +261,17 @@ def get_parent_progressive_item(item_name: str) -> str:
return _progressive_lookup.get(item_name, item_name)
@lru_cache
@cache_argsless
def get_vanilla() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_vanilla_logic())
@lru_cache
@cache_argsless
def get_sigma_normal() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_normal_logic())
@lru_cache
@cache_argsless
def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
+8 -4
View File
@@ -1,4 +1,3 @@
from functools import lru_cache
from math import floor
from pkgutil import get_data
from random import random
@@ -103,10 +102,15 @@ def parse_lambda(lambda_string) -> WitnessRule:
return lambda_set
@lru_cache(maxsize=None)
_adjustment_file_cache = dict()
def get_adjustment_file(adjustment_file: str) -> List[str]:
data = get_data(__name__, adjustment_file).decode("utf-8")
return [line.strip() for line in data.split("\n")]
if adjustment_file not in _adjustment_file_cache:
data = get_data(__name__, adjustment_file).decode("utf-8")
_adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")]
return _adjustment_file_cache[adjustment_file]
def get_disable_unrandomized_list() -> List[str]:
+1 -1
View File
@@ -5,7 +5,7 @@ from NetUtils import ClientStatus, NetworkItem
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
from worlds.yugioh06 import item_to_index
from . import item_to_index
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
+2 -2
View File
@@ -3,8 +3,8 @@ from typing import Dict, List, NamedTuple, Optional, Union
from BaseClasses import MultiWorld
from worlds.generic.Rules import CollectionRule
from worlds.yugioh06 import item_to_index, tier_1_opponents, yugioh06_difficulty
from worlds.yugioh06.locations import special
from . import item_to_index, tier_1_opponents, yugioh06_difficulty
from .locations import special
class OpponentData(NamedTuple):
+1 -1
View File
@@ -1,2 +1,2 @@
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@b36a23b5a138c78732ac8efb5b5ca8b0be07dcff#0.7.0
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@1dd2ce01c9d818caba5844529699b3ad026d6a07#0.7.1
typing-extensions>=4.7, <5