mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-14 11:33:47 -07:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aff9114c35 | ||
|
|
f656f08f9b | ||
|
|
967e3028fd | ||
|
|
428af55bd9 | ||
|
|
340725d395 | ||
|
|
f8030393c8 | ||
|
|
f6197d0a8d | ||
|
|
969ea5e6ee | ||
|
|
d4c6268a46 | ||
|
|
aeda76c058 | ||
|
|
9894d0672f | ||
|
|
d2e884b1d9 | ||
|
|
80b3a5b1d4 | ||
|
|
a6a9989fcf | ||
|
|
0c3b5439e9 | ||
|
|
963e9d4bb5 | ||
|
|
4dd7c63cab | ||
|
|
03a892aded | ||
|
|
b3c1c0bbe8 | ||
|
|
5a064b0979 | ||
|
|
f06e565441 | ||
|
|
41fdafa3fb | ||
|
|
27c528a6b3 | ||
|
|
9623c1fffd | ||
|
|
d4e0347d1d | ||
|
|
74bb057314 | ||
|
|
b2980178d1 | ||
|
|
08a0871168 | ||
|
|
51fa00399d | ||
|
|
7622f7f28f | ||
|
|
d98d693369 | ||
|
|
c7e8692964 | ||
|
|
0431c3fce0 | ||
|
|
411f0e40b6 | ||
|
|
a5d2046a87 | ||
|
|
f8893a7ed3 | ||
|
|
93ac018400 | ||
|
|
6b852d6e1a | ||
|
|
06dc76a78b | ||
|
|
4db4b5305e | ||
|
|
c550fdaee8 | ||
|
|
d13b7988b7 | ||
|
|
d437f0105a | ||
|
|
b65618030f | ||
|
|
01a2376b74 | ||
|
|
d10ddb17b6 | ||
|
|
c42d489bf7 | ||
|
|
8fef6b8d8c | ||
|
|
35b1178c20 | ||
|
|
c0f95755ff | ||
|
|
b7676a3da2 | ||
|
|
3d65719170 | ||
|
|
18d262c1ae | ||
|
|
e5fedb90a6 | ||
|
|
dc82b384c5 | ||
|
|
2f56e40fb7 | ||
|
|
d719eb356f | ||
|
|
6a34fe5184 | ||
|
|
461961c3be | ||
|
|
39869bcdc5 | ||
|
|
a10d7ae5b9 | ||
|
|
4ed45248eb | ||
|
|
6e4b255be5 | ||
|
|
2e56c226db | ||
|
|
844ff402cd | ||
|
|
ec570be178 | ||
|
|
3508cf21c7 | ||
|
|
1f4ddc295a | ||
|
|
4ef0e054d6 | ||
|
|
61310c50d7 | ||
|
|
6eab838a70 | ||
|
|
52e01c0925 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -155,4 +155,7 @@ Archipelago.zip
|
||||
|
||||
#minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft*/
|
||||
|
||||
#pyenv
|
||||
.python-version
|
||||
|
||||
@@ -782,10 +782,9 @@ class RegionType(int, Enum):
|
||||
|
||||
|
||||
class Region(object):
|
||||
|
||||
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
|
||||
def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.type = type_
|
||||
self.entrances = []
|
||||
self.exits = []
|
||||
self.locations = []
|
||||
@@ -1210,8 +1209,6 @@ class Spoiler():
|
||||
if self.world.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
for f_option, option in Options.common_options.items():
|
||||
write_option(f_option, option)
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.world.worlds[player].options
|
||||
|
||||
80
Fill.py
80
Fill.py
@@ -2,8 +2,10 @@ import logging
|
||||
import typing
|
||||
import collections
|
||||
import itertools
|
||||
from collections import Counter, deque
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld, Item
|
||||
from worlds.generic import PlandoItem
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
@@ -12,30 +14,35 @@ class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False,
|
||||
lock=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
def sweep_from_pool(base_state: CollectionState, itempool):
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: typing.List[Item],
|
||||
single_player_placement=False, lock=False):
|
||||
unplaced_items = []
|
||||
placements = []
|
||||
|
||||
reachable_items = {}
|
||||
swapped_items = Counter()
|
||||
reachable_items: dict[str, deque] = {}
|
||||
for item in itempool:
|
||||
reachable_items.setdefault(item.player, []).append(item)
|
||||
reachable_items.setdefault(item.player, deque()).append(item)
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
items_to_place = [items.pop() for items in reachable_items.values() if items] # grab one item per player
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
for item in items_to_place:
|
||||
itempool.remove(item)
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
maximum_exploration_state = sweep_from_pool(base_state, itempool)
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
spot_to_fill: Location = None
|
||||
if world.accessibility[item_to_place.player] == 'minimal':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) if single_player_placement else not has_beaten_game
|
||||
@@ -45,19 +52,48 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
|
||||
for i, location in enumerate(locations):
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
spot_to_fill = locations.pop(i) # poping by index is faster than removing by content,
|
||||
# poping by index is faster than removing by content,
|
||||
spot_to_fill = locations.pop(i)
|
||||
# skipping a scan for the element
|
||||
break
|
||||
|
||||
else:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.append(item_to_place)
|
||||
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
# we filled all reachable spots.
|
||||
# try swaping this item with previously placed items
|
||||
for(i, location) in enumerate(placements):
|
||||
placed_item = location.item
|
||||
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
||||
# number of times we will swap an individual item to prevent this
|
||||
if swapped_items[placed_item.player, placed_item.name] > 0:
|
||||
continue
|
||||
location.item = None
|
||||
placed_item.location = None
|
||||
swap_state = sweep_from_pool(base_state, itempool)
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(swap_state, item_to_place, perform_access_check):
|
||||
# Add this item to the exisiting placement, and
|
||||
# add the old item to the back of the queue
|
||||
spot_to_fill = placements.pop(i)
|
||||
swapped_items[placed_item.player,
|
||||
placed_item.name] += 1
|
||||
reachable_items[placed_item.player].appendleft(
|
||||
placed_item)
|
||||
itempool.append(placed_item)
|
||||
break
|
||||
else:
|
||||
# Item can't be placed here, restore original item
|
||||
location.item = placed_item
|
||||
placed_item.location = location
|
||||
|
||||
if spot_to_fill == None:
|
||||
# Maybe the game can be beaten anyway?
|
||||
unplaced_items.append(item_to_place)
|
||||
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
|
||||
continue
|
||||
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
|
||||
13
Generate.py
13
Generate.py
@@ -23,6 +23,7 @@ import Options
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
|
||||
categories = set(AutoWorldRegister.world_types)
|
||||
|
||||
@@ -148,7 +149,7 @@ def main(args=None, callback=ERmain):
|
||||
if category_name is None:
|
||||
weights_cache[path][key] = option
|
||||
elif category_name not in weights_cache[path]:
|
||||
raise Exception(f"Meta: Category {category_name} is not present in {path}.")
|
||||
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||
else:
|
||||
weights_cache[path][category_name][key] = option
|
||||
|
||||
@@ -330,7 +331,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
||||
|
||||
|
||||
def roll_linked_options(weights: dict) -> dict:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||
for option_set in weights["linked_options"]:
|
||||
if "name" not in option_set:
|
||||
raise ValueError("One of your linked options does not have a name.")
|
||||
@@ -352,7 +353,7 @@ def roll_linked_options(weights: dict) -> dict:
|
||||
|
||||
|
||||
def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||
weights["_Generator_Version"] = Utils.__version__
|
||||
for i, option_set in enumerate(triggers):
|
||||
try:
|
||||
@@ -469,7 +470,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
|
||||
ret = argparse.Namespace()
|
||||
for option_key in Options.per_game_common_options:
|
||||
if option_key in weights:
|
||||
if option_key in weights and option_key not in Options.common_options:
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
@@ -491,7 +492,9 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
|
||||
for option_key, option in world_type.options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
# skip setting this option if already set from common_options, defaulting to root option
|
||||
if not (option_key in Options.common_options and option_key not in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
if "items" in plando_options:
|
||||
ret.plando_items = roll_item_plando(world_type, game_weights)
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
|
||||
@@ -235,11 +235,11 @@ class Context:
|
||||
with open(multidatapath, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
self._load(self._decompress(data), use_embedded_server_options)
|
||||
self._load(self.decompress(data), use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
@staticmethod
|
||||
def _decompress(data: bytes) -> dict:
|
||||
def decompress(data: bytes) -> dict:
|
||||
format_version = data[0]
|
||||
if format_version != 1:
|
||||
raise Exception("Incompatible multidata.")
|
||||
|
||||
@@ -12,6 +12,7 @@ from worlds.oot.Cosmetics import patch_cosmetics
|
||||
from worlds.oot.Options import cosmetic_options, sfx_options
|
||||
from worlds.oot.Rom import Rom, compress_rom_file
|
||||
from worlds.oot.N64Patch import apply_patch_file
|
||||
from worlds.oot.Utils import data_path
|
||||
from Utils import local_path
|
||||
|
||||
logger = logging.getLogger('OoTAdjuster')
|
||||
@@ -211,9 +212,11 @@ def adjust(args):
|
||||
ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless'
|
||||
ootworld.death_link = args.deathlink
|
||||
|
||||
delete_zootdec = False
|
||||
if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']:
|
||||
# Load up the ROM
|
||||
rom = Rom(file=args.rom, force_use=True)
|
||||
delete_zootdec = True
|
||||
elif os.path.splitext(args.rom)[-1] == '.apz5':
|
||||
# Load vanilla ROM
|
||||
rom = Rom(file=args.vanilla_rom, force_use=True)
|
||||
@@ -222,15 +225,21 @@ def adjust(args):
|
||||
else:
|
||||
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
|
||||
# Call patch_cosmetics
|
||||
patch_cosmetics(ootworld, rom)
|
||||
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
|
||||
# Output new file
|
||||
path_pieces = os.path.splitext(args.rom)
|
||||
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
|
||||
comp_path = path_pieces[0] + '-adjusted.n64'
|
||||
rom.write_to_file(decomp_path)
|
||||
compress_rom_file(decomp_path, comp_path)
|
||||
os.remove(decomp_path)
|
||||
try:
|
||||
patch_cosmetics(ootworld, rom)
|
||||
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
|
||||
# Output new file
|
||||
path_pieces = os.path.splitext(args.rom)
|
||||
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
|
||||
comp_path = path_pieces[0] + '-adjusted.n64'
|
||||
rom.write_to_file(decomp_path)
|
||||
os.chdir(data_path("Compress"))
|
||||
compress_rom_file(decomp_path, comp_path)
|
||||
os.remove(decomp_path)
|
||||
finally:
|
||||
if delete_zootdec:
|
||||
os.chdir(os.path.split(__file__)[0])
|
||||
os.remove("ZOOTDEC.z64")
|
||||
return comp_path
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -125,6 +125,8 @@ class Toggle(Option):
|
||||
def get_option_name(cls, value):
|
||||
return ["No", "Yes"][int(value)]
|
||||
|
||||
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||
|
||||
|
||||
class DefaultOnToggle(Toggle):
|
||||
default = 1
|
||||
@@ -184,6 +186,8 @@ class Choice(Option):
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||
|
||||
|
||||
class Range(Option, int):
|
||||
range_start = 0
|
||||
@@ -334,7 +338,7 @@ class Accessibility(Choice):
|
||||
Locations: ensure everything can be reached and acquired.
|
||||
Items: ensure all logically relevant items can be acquired.
|
||||
Minimal: ensure what is needed to reach your goal can be acquired."""
|
||||
|
||||
displayname = "Accessibility"
|
||||
option_locations = 0
|
||||
option_items = 1
|
||||
option_minimal = 2
|
||||
@@ -344,6 +348,7 @@ class Accessibility(Choice):
|
||||
|
||||
class ProgressionBalancing(DefaultOnToggle):
|
||||
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
|
||||
displayname = "Progression Balancing"
|
||||
|
||||
|
||||
common_options = {
|
||||
@@ -395,6 +400,7 @@ class DeathLink(Toggle):
|
||||
|
||||
|
||||
per_game_common_options = {
|
||||
**common_options, # can be overwritten per-game
|
||||
"local_items": LocalItems,
|
||||
"non_local_items": NonLocalItems,
|
||||
"start_inventory": StartInventory,
|
||||
|
||||
@@ -14,6 +14,7 @@ Currently, the following games are supported:
|
||||
* Super Metroid
|
||||
* Secret of Evermore
|
||||
* Final Fantasy
|
||||
* Rogue Legacy
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
15
SNIClient.py
15
SNIClient.py
@@ -523,18 +523,21 @@ def launch_sni(ctx: Context):
|
||||
if not os.path.isdir(sni_path):
|
||||
sni_path = Utils.local_path(sni_path)
|
||||
if os.path.isdir(sni_path):
|
||||
for file in os.listdir(sni_path):
|
||||
lower_file = file.lower()
|
||||
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni":
|
||||
sni_path = os.path.join(sni_path, file)
|
||||
dir_entry: os.DirEntry
|
||||
for dir_entry in os.scandir(sni_path):
|
||||
if dir_entry.is_file():
|
||||
lower_file = dir_entry.name.lower()
|
||||
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or (lower_file == "sni"):
|
||||
sni_path = dir_entry.path
|
||||
break
|
||||
|
||||
if os.path.isfile(sni_path):
|
||||
snes_logger.info(f"Attempting to start {sni_path}")
|
||||
import sys
|
||||
if not sys.stdout: # if it spawns a visible console, may as well populate it
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
|
||||
subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path))
|
||||
else:
|
||||
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL,
|
||||
subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
snes_logger.info(
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
|
||||
build: int
|
||||
|
||||
|
||||
__version__ = "0.2.2"
|
||||
__version__ = "0.2.3"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
from yaml import load, dump, safe_load
|
||||
|
||||
@@ -89,6 +89,11 @@ def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template(f"weighted-settings.html")
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
@@ -188,6 +193,15 @@ def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
|
||||
@app.route('/datapackage')
|
||||
@cache.cached()
|
||||
def get_datapackge():
|
||||
"""A pretty print version of /api/datapackage"""
|
||||
from worlds import network_data_package
|
||||
import json
|
||||
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
||||
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ def get_datapackge():
|
||||
from worlds import network_data_package
|
||||
return network_data_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
def get_datapackge_versions():
|
||||
|
||||
@@ -76,7 +76,7 @@ class WebHostContext(Context):
|
||||
else:
|
||||
self.port = get_random_port()
|
||||
|
||||
return self._load(self._decompress(room.seed.multidata), True)
|
||||
return self._load(self.decompress(room.seed.multidata), True)
|
||||
|
||||
@db_session
|
||||
def init_save(self, enabled: bool = True):
|
||||
|
||||
@@ -11,6 +11,8 @@ target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||
|
||||
|
||||
def create():
|
||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||
|
||||
def dictify_range(option):
|
||||
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
|
||||
option.default: 50}
|
||||
@@ -25,15 +27,24 @@ def create():
|
||||
return list(default_value)
|
||||
return default_value
|
||||
|
||||
weighted_settings = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "Player",
|
||||
"game": {},
|
||||
},
|
||||
"games": {},
|
||||
}
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options = {**world.options, **Options.per_game_common_options}
|
||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||
options={**world.options, **Options.per_game_common_options},
|
||||
options=all_options,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
dictify_range=dictify_range, default_converter=default_converter,
|
||||
)
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||
f.write(res)
|
||||
|
||||
@@ -47,7 +58,7 @@ def create():
|
||||
}
|
||||
|
||||
game_options = {}
|
||||
for option_name, option in world.options.items():
|
||||
for option_name, option in all_options.items():
|
||||
if option.options:
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
@@ -86,4 +97,14 @@ def create():
|
||||
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
||||
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
||||
|
||||
if not world.hidden:
|
||||
weighted_settings["baseOptions"]["game"][game_name] = 0
|
||||
weighted_settings["games"][game_name] = {}
|
||||
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
||||
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
|
||||
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
|
||||
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))
|
||||
|
||||
22
WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md
Normal file
22
WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Rogue Legacy (PC)
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
You are not able to buy skill upgrades in the manor upgrade screen, and instead, need to find them in order to level up
|
||||
your character to make fighting the 5 bosses easier.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen,
|
||||
diary checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the
|
||||
finding of stats less of a chore. Runes and Equipment are also grouped together.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, your character will hold the item above their head and display it to the world. It's
|
||||
good for business!
|
||||
@@ -74,7 +74,7 @@ plando_items:
|
||||
- item:
|
||||
Empire Orb: 1
|
||||
Radiant Orb: 1
|
||||
location: Starter Chest 1
|
||||
location: Starter chest 1
|
||||
from_pool: true
|
||||
world: true
|
||||
percentage: 50
|
||||
@@ -177,4 +177,4 @@ when you leave the interior you will exit to the cave 45 ledge. Going into the c
|
||||
lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take you to
|
||||
their locations as normal but leaving old man cave will exit at Agahnim's Tower.
|
||||
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the Minecraft
|
||||
connection plando to work structure shuffle must be enabled.
|
||||
connection plando to work structure shuffle must be enabled.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Rogue Legacy Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/releases)
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how
|
||||
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
|
||||
each player to enjoy an experience customized for their taste, and different players in the same multiworld
|
||||
can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
you can customize your settings by visiting the <a href="/games/Rogue Legacy/player-settings">rogue legacy settings page here</a>.
|
||||
|
||||
### Connect to the MultiServer
|
||||
Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port,
|
||||
slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server
|
||||
provides an alternative one to the default values.
|
||||
|
||||
### Play the game
|
||||
Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen.
|
||||
Now you're off to start your legacy!
|
||||
|
||||
## Manual Installation
|
||||
In order to run Rogue Legacy Randomizer you will need to have Rogue Legacy installed on your local machine. Extract the
|
||||
Randomizer release into a desired folder **outside** of your Rogue Legacy install. Copy the following files from your
|
||||
Rogue Legacy install into the main directory of your Rogue Legacy Randomizer install:
|
||||
|
||||
- DS2DEngine.dll
|
||||
- InputSystem.dll
|
||||
- Nuclex.Input.dll
|
||||
- SpriteSystem.dll
|
||||
- Tweener.dll
|
||||
|
||||
And copy the directory from your Rogue Legacy install as well into the main directory of your Rogue Legacy Randomizer
|
||||
install:
|
||||
|
||||
- Content/
|
||||
|
||||
Then copy the contents of the CustomContent directory in your Rogue Legacy Randomizer into the newly copied Content
|
||||
directory and overwrite all files.
|
||||
|
||||
**BE SURE YOU ARE REPLACING THE COPIED FILES IN YOUR ROGUE LEGACY RANDOMIZER DIRECTORY AND NOT REPLACING YOUR ROGUE
|
||||
LEGACY FILES!**
|
||||
@@ -1,126 +1,143 @@
|
||||
# Super Metroid Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [Super Metroid Client](https://github.com/ArchipelagoMW/SuperMetroidClient/releases)
|
||||
- **sniConnector.lua** (located on the client download page)
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in the Super Metroid Client)
|
||||
|
||||
- SNI Client
|
||||
- Included in Archipelago download
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
|
||||
- Your Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc`
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x Multitroid
|
||||
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
compatible hardware
|
||||
- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc`
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
### Windows Setup
|
||||
1. Download and install the Super Metroid Client from the link above, making sure to install the most recent version.
|
||||
**The file is located in the assets section at the bottom of the version information**.
|
||||
|
||||
1. During the installation of Archipelago, you will have been asked to install the SNI Client.
|
||||
If you did not do this, or you are on an older version, you may run the installer again to install the SNI Client.
|
||||
2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file.
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program
|
||||
for launching ROM files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
|
||||
files.
|
||||
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
|
||||
2. Right-click on a ROM file and select **Open with...**
|
||||
3. Check the box next to **Always use this app to open .sfc files**
|
||||
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside
|
||||
the folder you extracted in step one.
|
||||
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
|
||||
extracted in step one.
|
||||
|
||||
### Macintosh Setup
|
||||
|
||||
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
|
||||
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a config file and why do I need one?
|
||||
Your config file contains a set of configuration options which provide the generator with information about how
|
||||
it should generate your game. Each player of a multiworld will provide their own config file. This setup allows
|
||||
each player to enjoy an experience customized for their taste, and different players in the same multiworld
|
||||
can all have different options.
|
||||
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
The [Player Settings](/games/Super%20Metroid/player-settings) page on the website allows you to configure your
|
||||
personal settings and export a config file from them.
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
|
||||
|
||||
### Verifying your config file
|
||||
If you would like to validate your config file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
1. Navigate to the [Player Settings](/games/Super%20Metroid/player-settings) page, configure your options, and click
|
||||
the "Generate Game" button.
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from
|
||||
the patch file, and open your emulator for you.
|
||||
5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from the
|
||||
patch file, and open your emulator for you.
|
||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your patch file and create your ROM
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that
|
||||
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
|
||||
everyone's patch files. Your patch file should have a `.apm3` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically
|
||||
launch the client, and will also create your ROM in the same place as your patch file.
|
||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||
files. Your patch file should have a `.apm3` extension.
|
||||
|
||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||
client, and will also create your ROM in the same place as your patch file.
|
||||
|
||||
### Connect to the client
|
||||
|
||||
#### With an emulator
|
||||
When the client launched automatically, SNI should have also automatically launched in the background.
|
||||
If this is its first time launching, you may be prompted to allow it to communicate through the Windows
|
||||
Firewall.
|
||||
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
5. Select the `Connector.lua` file in the `Archipelago\SNI\lua` folder.
|
||||
- Use x86 for 32-bit or x64 for 64-bit.
|
||||
|
||||
##### BizHawk
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||
these menu options:
|
||||
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
|
||||
menu options:
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Once you have changed the loaded core, you must restart BizHawk.
|
||||
2. Load your ROM file if it hasn't already been loaded.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click the button to open a new Lua script.
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
5. Select the `Connector.lua` file in `Archipelago\SNI\lua` folder.
|
||||
- Use x86 for 32-bit or x64 for 64-bit. Please note the most recent versions of BizHawk are 64-bit only.
|
||||
|
||||
#### With hardware
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||
done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware
|
||||
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
||||
[on this page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
|
||||
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
|
||||
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
|
||||
|
||||
Other hardware may find helpful information on the usb2snes platforms
|
||||
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Power on your device and load the ROM.
|
||||
|
||||
### Connect to the Archipelago Server
|
||||
The patch file which launched your client should have automatically connected you to the AP Server.
|
||||
There are a few reasons this may not happen however, including if the game is hosted on the website but
|
||||
was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host
|
||||
for the address of the server, and copy/paste it into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server
|
||||
Status: Connected".
|
||||
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
|
||||
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
|
||||
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
|
||||
into the "Server" input field then press enter.
|
||||
|
||||
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
|
||||
|
||||
### Play the game
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game!
|
||||
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
|
||||
successfully joining a multiworld game!
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
|
||||
|
||||
The recommended way to host a game is to use our hosting service. The process is relatively simple:
|
||||
|
||||
1. Collect config files from your players.
|
||||
2. Create a zip file containing your players' config files.
|
||||
3. Upload that zip file to the website linked above.
|
||||
3. Upload that zip file to the Generate page above.
|
||||
- Generate page: [WebHost Seed Generation Page](/generate)
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||
so they may download their patch files from there.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
|
||||
they may download their patch files from there.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
|
||||
players in the game. Any observers may also be given the link to this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner)
|
||||
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (humble)](https://www.humblebundle.com/store/timespinner) or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported)
|
||||
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## General Concept
|
||||
@@ -11,7 +11,7 @@ The timespinner Randomizer loads Timespinner.exe from the same folder, and alter
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the [readme](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on windows) or TsRandomizerItemTracker.bin.x86_64 (on linux) or TsRandomizerItemTracker.bin.osx (on mac) instead of Timespinner.exe to start the game in randomized mode, for more info see the [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
@@ -23,38 +23,8 @@ Download latest version of [Timespinner Randomizer](https://github.com/JarnoWest
|
||||
5. Select "Connect"
|
||||
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty
|
||||
|
||||
## YAML Settings
|
||||
An example YAML would look like this:
|
||||
```yaml
|
||||
description: Default Timespinner Template
|
||||
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
game:
|
||||
Timespinner: 1
|
||||
requires:
|
||||
version: 0.1.8
|
||||
Timespinner:
|
||||
StartWithJewelryBox: # Start with Jewelry Box unlocked
|
||||
false: 50
|
||||
true: 0
|
||||
DownloadableItems: # With the tablet you will be able to download items at terminals
|
||||
false: 50
|
||||
true: 50
|
||||
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
|
||||
false: 50
|
||||
true: 0
|
||||
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
|
||||
false: 50
|
||||
true: 50
|
||||
QuickSeed: # Start with Talaria Attachment, Nyoom!
|
||||
false: 50
|
||||
true: 0
|
||||
SpecificKeycards: # Keycards can only open corresponding doors
|
||||
false: 0
|
||||
true: 50
|
||||
Inverted: # Start in the past
|
||||
false: 50
|
||||
true: 50
|
||||
```
|
||||
* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight
|
||||
## Where do I get a config file?
|
||||
The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to configure your personal settings and export a config file from them.
|
||||
|
||||
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
||||
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds
|
||||
@@ -342,5 +342,24 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Rogue Legacy",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, multiworld, and related software.",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "rogue-legacy/rogue-legacy_en.md",
|
||||
"link": "rogue-legacy/rogue-legacy/en",
|
||||
"authors": [
|
||||
"Phar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# A Link to the Past Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with
|
||||
[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- If installing Archipelago, make sure to check the box for SNIClient -> A Link to the Past Patch Setup during install, or SNI will not be included
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and SNIClient)
|
||||
- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases) included with the main Archipelago install
|
||||
or [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/releases)
|
||||
- If installing Archipelago, make sure to check the box for `SNI Client - A Link to the Past Patch Setup`
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in both clients from the first step)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
@@ -75,8 +75,9 @@ Firewall.
|
||||
3. Click on **New Lua Script Window...**
|
||||
4. In the new window, click **Browse...**
|
||||
5. Select the connector lua file included with your client
|
||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/sni/lua`
|
||||
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if
|
||||
the emulator is 64-bit or 32-bit.
|
||||
|
||||
##### BizHawk
|
||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||
@@ -87,9 +88,9 @@ Firewall.
|
||||
3. Click on the Tools menu and click on **Lua Console**
|
||||
4. Click Script -> Open Script...
|
||||
5. Select the `Connector.lua` file you downloaded above
|
||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/sni/lua`
|
||||
6. Run the script by double-clicking it in the listing
|
||||
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
|
||||
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if
|
||||
the emulator is 64-bit or 32-bit.
|
||||
|
||||
#### With hardware
|
||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||
@@ -111,7 +112,8 @@ Status: Connected".
|
||||
|
||||
### Play the game
|
||||
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations
|
||||
on successfully joining a multiworld game!
|
||||
on successfully joining a multiworld game! You can execute various commands in your client. For more information
|
||||
regarding these commands you can use `/help` for local client commands and `!help` for server commands.
|
||||
|
||||
## Hosting a MultiWorld game
|
||||
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
|
||||
|
||||
588
WebHostLib/static/assets/weighted-settings.js
Normal file
588
WebHostLib/static/assets/weighted-settings.js
Normal file
@@ -0,0 +1,588 @@
|
||||
window.addEventListener('load', () => {
|
||||
fetchSettingData().then((results) => {
|
||||
let settingHash = localStorage.getItem('weighted-settings-hash');
|
||||
if (!settingHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
localStorage.setItem('weighted-settings-hash', md5(results));
|
||||
localStorage.removeItem('weighted-settings');
|
||||
settingHash = md5(results);
|
||||
}
|
||||
|
||||
if (settingHash !== md5(results)) {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.";
|
||||
userMessage.style.display = "block";
|
||||
userMessage.addEventListener('click', resetSettings);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultSettings(results);
|
||||
buildUI(results);
|
||||
updateVisibleGames();
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.setAttribute('data-type', 'data');
|
||||
nameInput.setAttribute('data-setting', 'name');
|
||||
nameInput.addEventListener('keyup', updateBaseSetting);
|
||||
nameInput.value = weightedSettings.name;
|
||||
});
|
||||
});
|
||||
|
||||
const resetSettings = () => {
|
||||
localStorage.removeItem('weighted-settings');
|
||||
localStorage.removeItem('weighted-settings-hash')
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => {
|
||||
try{ resolve(response.json()); }
|
||||
catch(error){ reject(error); }
|
||||
});
|
||||
});
|
||||
|
||||
const createDefaultSettings = (settingData) => {
|
||||
if (!localStorage.getItem('weighted-settings')) {
|
||||
const newSettings = {};
|
||||
|
||||
// Transfer base options directly
|
||||
for (let baseOption of Object.keys(settingData.baseOptions)){
|
||||
newSettings[baseOption] = settingData.baseOptions[baseOption];
|
||||
}
|
||||
|
||||
// Set options per game
|
||||
for (let game of Object.keys(settingData.games)) {
|
||||
// Initialize game object
|
||||
newSettings[game] = {};
|
||||
|
||||
// Transfer game settings
|
||||
for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){
|
||||
newSettings[game][gameSetting] = {};
|
||||
|
||||
const setting = settingData.games[game].gameSettings[gameSetting];
|
||||
switch(setting.type){
|
||||
case 'select':
|
||||
setting.options.forEach((option) => {
|
||||
newSettings[game][gameSetting][option.value] =
|
||||
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
|
||||
});
|
||||
break;
|
||||
case 'range':
|
||||
for (let i = setting.min; i <= setting.max; ++i){
|
||||
newSettings[game][gameSetting][i] =
|
||||
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0;
|
||||
}
|
||||
newSettings[game][gameSetting]['random'] = 0;
|
||||
newSettings[game][gameSetting]['random-low'] = 0;
|
||||
newSettings[game][gameSetting]['random-high'] = 0;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
newSettings[game].start_inventory = [];
|
||||
newSettings[game].exclude_locations = [];
|
||||
newSettings[game].local_items = [];
|
||||
newSettings[game].non_local_items = [];
|
||||
newSettings[game].start_hints = [];
|
||||
}
|
||||
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints
|
||||
// TODO: Include location configs: exclude_locations
|
||||
const buildUI = (settingData) => {
|
||||
// Build the game-choice div
|
||||
buildGameChoice(settingData.games);
|
||||
|
||||
const gamesWrapper = document.getElementById('games-wrapper');
|
||||
Object.keys(settingData.games).forEach((game) => {
|
||||
// Create game div, invisible by default
|
||||
const gameDiv = document.createElement('div');
|
||||
gameDiv.setAttribute('id', `${game}-div`);
|
||||
gameDiv.classList.add('game-div');
|
||||
gameDiv.classList.add('invisible');
|
||||
|
||||
const gameHeader = document.createElement('h2');
|
||||
gameHeader.innerText = game;
|
||||
gameDiv.appendChild(gameHeader);
|
||||
|
||||
const collapseButton = document.createElement('a');
|
||||
collapseButton.innerText = '(Collapse)';
|
||||
gameDiv.appendChild(collapseButton);
|
||||
|
||||
const expandButton = document.createElement('a');
|
||||
expandButton.innerText = '(Expand)';
|
||||
expandButton.classList.add('invisible');
|
||||
gameDiv.appendChild(expandButton);
|
||||
|
||||
const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings);
|
||||
gameDiv.appendChild(optionsDiv);
|
||||
gamesWrapper.appendChild(gameDiv);
|
||||
|
||||
collapseButton.addEventListener('click', () => {
|
||||
collapseButton.classList.add('invisible');
|
||||
optionsDiv.classList.add('invisible');
|
||||
expandButton.classList.remove('invisible');
|
||||
});
|
||||
|
||||
expandButton.addEventListener('click', () => {
|
||||
collapseButton.classList.remove('invisible');
|
||||
optionsDiv.classList.remove('invisible');
|
||||
expandButton.classList.add('invisible');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const buildGameChoice = (games) => {
|
||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const gameChoiceDiv = document.getElementById('game-choice');
|
||||
const h2 = document.createElement('h2');
|
||||
h2.innerText = 'Game Select';
|
||||
gameChoiceDiv.appendChild(h2);
|
||||
|
||||
const gameSelectDescription = document.createElement('p');
|
||||
gameSelectDescription.classList.add('setting-description');
|
||||
gameSelectDescription.innerText = 'Choose which games you might be required to play.';
|
||||
gameChoiceDiv.appendChild(gameSelectDescription);
|
||||
|
||||
const hintText = document.createElement('p');
|
||||
hintText.classList.add('hint-text');
|
||||
hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' +
|
||||
'to that section.'
|
||||
gameChoiceDiv.appendChild(hintText);
|
||||
|
||||
// Build the game choice table
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(games).forEach((game) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
const span = document.createElement('span');
|
||||
span.innerText = game;
|
||||
span.setAttribute('id', `${game}-game-option`)
|
||||
tdLeft.appendChild(span);
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.setAttribute('data-type', 'weight');
|
||||
range.setAttribute('data-setting', 'game');
|
||||
range.setAttribute('data-option', game);
|
||||
range.value = settings.game[game];
|
||||
range.addEventListener('change', (evt) => {
|
||||
updateBaseSetting(evt);
|
||||
updateVisibleGames(); // Show or hide games based on the new settings
|
||||
});
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `game-${game}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
gameChoiceDiv.appendChild(table);
|
||||
};
|
||||
|
||||
const buildOptionsDiv = (game, settings) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const optionsWrapper = document.createElement('div');
|
||||
optionsWrapper.classList.add('settings-wrapper');
|
||||
|
||||
Object.keys(settings).forEach((settingName) => {
|
||||
const setting = settings[settingName];
|
||||
const settingWrapper = document.createElement('div');
|
||||
settingWrapper.classList.add('setting-wrapper');
|
||||
|
||||
const settingNameHeader = document.createElement('h4');
|
||||
settingNameHeader.innerText = setting.displayName;
|
||||
settingWrapper.appendChild(settingNameHeader);
|
||||
|
||||
const settingDescription = document.createElement('p');
|
||||
settingDescription.classList.add('setting-description');
|
||||
settingDescription.innerText = setting.description.replace(/(\n)/g, ' ');
|
||||
settingWrapper.appendChild(settingDescription);
|
||||
|
||||
switch(setting.type){
|
||||
case 'select':
|
||||
const optionTable = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
// Add a weight range for each option
|
||||
setting.options.forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option.name;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option.value);
|
||||
range.setAttribute('data-type', setting.type);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][option.value];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${option.value}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
optionTable.appendChild(tbody);
|
||||
settingWrapper.appendChild(optionTable);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
const hintText = document.createElement('p');
|
||||
hintText.classList.add('hint-text');
|
||||
hintText.innerHTML = 'This is a range option. You may enter valid numerical values in the text box below, ' +
|
||||
`then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
|
||||
`Maximum value: ${setting.max}`;
|
||||
settingWrapper.appendChild(hintText);
|
||||
|
||||
const addOptionDiv = document.createElement('div');
|
||||
addOptionDiv.classList.add('add-option-div');
|
||||
const optionInput = document.createElement('input');
|
||||
optionInput.setAttribute('id', `${game}-${settingName}-option`);
|
||||
optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
|
||||
addOptionDiv.appendChild(optionInput);
|
||||
const addOptionButton = document.createElement('button');
|
||||
addOptionButton.innerText = 'Add';
|
||||
addOptionDiv.appendChild(addOptionButton);
|
||||
settingWrapper.appendChild(addOptionDiv);
|
||||
optionInput.addEventListener('keydown', (evt) => {
|
||||
if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
|
||||
});
|
||||
|
||||
const rangeTable = document.createElement('table');
|
||||
const rangeTbody = document.createElement('tbody');
|
||||
|
||||
if (((setting.max - setting.min) + 1) < 11) {
|
||||
for (let i=setting.min; i <= setting.max; ++i) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = i;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', i);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][i];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
}
|
||||
} else {
|
||||
Object.keys(currentSettings[game][settingName]).forEach((option) => {
|
||||
if (currentSettings[game][settingName][option] > 0) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
range.dispatchEvent(new Event('change'));
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
['random', 'random-low', 'random-high'].forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][option];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
rangeTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
rangeTable.appendChild(rangeTbody);
|
||||
settingWrapper.appendChild(rangeTable);
|
||||
|
||||
addOptionButton.addEventListener('click', () => {
|
||||
const optionInput = document.getElementById(`${game}-${settingName}-option`);
|
||||
let option = optionInput.value;
|
||||
if (!option || !option.trim()) { return; }
|
||||
option = parseInt(option, 10);
|
||||
if ((option < setting.min) || (option > setting.max)) { return; }
|
||||
optionInput.value = '';
|
||||
if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; }
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
const tdLeft = document.createElement('td');
|
||||
tdLeft.classList.add('td-left');
|
||||
tdLeft.innerText = option;
|
||||
tr.appendChild(tdLeft);
|
||||
|
||||
const tdMiddle = document.createElement('td');
|
||||
tdMiddle.classList.add('td-middle');
|
||||
const range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('id', `${game}-${settingName}-${option}-range`);
|
||||
range.setAttribute('data-game', game);
|
||||
range.setAttribute('data-setting', settingName);
|
||||
range.setAttribute('data-option', option);
|
||||
range.setAttribute('min', 0);
|
||||
range.setAttribute('max', 50);
|
||||
range.addEventListener('change', updateGameSetting);
|
||||
range.value = currentSettings[game][settingName][parseInt(option, 10)];
|
||||
tdMiddle.appendChild(range);
|
||||
tr.appendChild(tdMiddle);
|
||||
|
||||
const tdRight = document.createElement('td');
|
||||
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
|
||||
tdRight.classList.add('td-right');
|
||||
tdRight.innerText = range.value;
|
||||
tr.appendChild(tdRight);
|
||||
|
||||
const tdDelete = document.createElement('td');
|
||||
tdDelete.classList.add('td-delete');
|
||||
const deleteButton = document.createElement('span');
|
||||
deleteButton.classList.add('range-option-delete');
|
||||
deleteButton.innerText = '❌';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
range.value = 0;
|
||||
range.dispatchEvent(new Event('change'));
|
||||
rangeTbody.removeChild(tr);
|
||||
});
|
||||
tdDelete.appendChild(deleteButton);
|
||||
tr.appendChild(tdDelete);
|
||||
|
||||
rangeTbody.appendChild(tr);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
optionsWrapper.appendChild(settingWrapper);
|
||||
});
|
||||
|
||||
return optionsWrapper;
|
||||
};
|
||||
|
||||
const updateVisibleGames = () => {
|
||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
Object.keys(settings.game).forEach((game) => {
|
||||
const gameDiv = document.getElementById(`${game}-div`);
|
||||
const gameOption = document.getElementById(`${game}-game-option`);
|
||||
if (parseInt(settings.game[game], 10) > 0) {
|
||||
gameDiv.classList.remove('invisible');
|
||||
gameOption.classList.add('jump-link');
|
||||
gameOption.addEventListener('click', () => {
|
||||
const gameDiv = document.getElementById(`${game}-div`);
|
||||
if (gameDiv.classList.contains('invisible')) { return; }
|
||||
gameDiv.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
gameDiv.classList.add('invisible');
|
||||
gameOption.classList.remove('jump-link');
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateBaseSetting = (event) => {
|
||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const setting = event.target.getAttribute('data-setting');
|
||||
const option = event.target.getAttribute('data-option');
|
||||
const type = event.target.getAttribute('data-type');
|
||||
|
||||
switch(type){
|
||||
case 'weight':
|
||||
settings[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
|
||||
document.getElementById(`${setting}-${option}`).innerText = event.target.value;
|
||||
break;
|
||||
case 'data':
|
||||
settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
|
||||
break;
|
||||
}
|
||||
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
||||
};
|
||||
|
||||
const updateGameSetting = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
const game = event.target.getAttribute('data-game');
|
||||
const setting = event.target.getAttribute('data-setting');
|
||||
const option = event.target.getAttribute('data-option');
|
||||
const type = event.target.getAttribute('data-type');
|
||||
document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value;
|
||||
options[game][setting][option] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||
if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up the settings output
|
||||
Object.keys(settings.game).forEach((game) => {
|
||||
// Remove any disabled games
|
||||
if (settings.game[game] === 0) {
|
||||
delete settings.game[game];
|
||||
delete settings[game];
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any disabled options
|
||||
Object.keys(settings[game]).forEach((setting) => {
|
||||
Object.keys(settings[game][setting]).forEach((option) => {
|
||||
if (settings[game][setting][option] === 0) {
|
||||
delete settings[game][setting][option];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: localStorage.getItem('weighted-settings') },
|
||||
presetData: { player: localStorage.getItem('weighted-settings') },
|
||||
playerCount: 1,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage.innerText += ' ' + error.response.data.text;
|
||||
}
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
@@ -18,3 +18,34 @@
|
||||
border-radius: 3px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
#host-room table {
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
#host-room table tbody{
|
||||
background-color: #dce2bd;
|
||||
}
|
||||
|
||||
#host-room table tbody tr:hover{
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
#host-room table tbody td{
|
||||
padding: 4px 6px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#host-room table tbody a{
|
||||
color: #234ae4;
|
||||
}
|
||||
|
||||
#host-room table thead td{
|
||||
background-color: #b0a77d;
|
||||
color: black;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#host-room table tbody td{
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
191
WebHostLib/static/styles/weighted-settings.css
Normal file
191
WebHostLib/static/styles/weighted-settings.css
Normal file
@@ -0,0 +1,191 @@
|
||||
html{
|
||||
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
|
||||
background-repeat: repeat;
|
||||
background-size: 650px 650px;
|
||||
scroll-padding-top: 90px;
|
||||
}
|
||||
|
||||
#weighted-settings{
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#weighted-settings #games-wrapper{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper{
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button{
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 0 0 0.15rem;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#weighted-settings .setting-wrapper .add-option-div button:active{
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#weighted-settings p.setting-description{
|
||||
font-weight: bold;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings p.hint-text{
|
||||
margin: 0 0 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#weighted-settings .jump-link{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#weighted-settings table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-left{
|
||||
padding-right: 1rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-middle{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-right{
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .td-delete{
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#weighted-settings table .range-option-delete{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings .game-options, #weighted-settings .rom-options{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px), all and (orientation: portrait){
|
||||
#weighted-settings .game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,43 @@
|
||||
{%- endmacro %}
|
||||
{% macro list_patches_room(room) %}
|
||||
{% if room.seed.slots %}
|
||||
<ul>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td>Name</td>
|
||||
<td>Game</td>
|
||||
<td>Download Link</td>
|
||||
<td>Tracker Page</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
|
||||
APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
|
||||
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td>{{ patch.player_name }}</td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMC File...</a>
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download Factorio Mod...</a>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APZ5 File...</a>
|
||||
{% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid"] %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% else %}
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
@@ -29,13 +29,6 @@ game:
|
||||
requires:
|
||||
version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
|
||||
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
|
||||
progression_balancing:
|
||||
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
|
||||
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
|
||||
{%- macro range_option(option) %}
|
||||
# you can add additional values between minimum and maximum
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, description in worlds.items() %}
|
||||
{% for game, description in worlds.items() | sort %}
|
||||
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||
<p>
|
||||
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Generate Game</title>
|
||||
<title>User Content</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/userContent.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/userContent.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
42
WebHostLib/templates/weighted-settings.html
Normal file
42
WebHostLib/templates/weighted-settings.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-settings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-settings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||
this page, or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
|
||||
<div id="game-choice">
|
||||
<!-- User chooses games by weight -->
|
||||
</div>
|
||||
|
||||
<!-- To be generated and populated per-game with weight > 0 -->
|
||||
<div id="games-wrapper">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -252,7 +252,7 @@ def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if result:
|
||||
return result
|
||||
multidata = Context._decompress(room.seed.multidata)
|
||||
multidata = Context.decompress(room.seed.multidata)
|
||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||
locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations']
|
||||
names: Dict[int, Dict[int, str]] = multidata["names"]
|
||||
|
||||
@@ -62,12 +62,20 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
elif file.filename.endswith(".archipelago"):
|
||||
try:
|
||||
multidata = zfile.open(file).read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
multidata = None
|
||||
|
||||
if multidata:
|
||||
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
||||
player_names = {slot.player_name for slot in slots}
|
||||
leftover_names = [(name, index) for index, name in
|
||||
enumerate((name for name in decompressed_multidata["names"][0]), start=1)]
|
||||
newslots = [(Slot(data=None, player_name=name, player_id=slot, game=decompressed_multidata["games"][slot]))
|
||||
for name, slot in leftover_names if name not in player_names]
|
||||
for slot in newslots:
|
||||
slots.add(slot)
|
||||
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||
id=sid if sid else uuid.uuid4())
|
||||
@@ -92,7 +100,7 @@ def uploads():
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
res = upload_zip_to_db(zfile)
|
||||
if type(res) == str:
|
||||
@@ -100,12 +108,12 @@ def uploads():
|
||||
elif res:
|
||||
return redirect(url_for("view_seed", seed=res.id))
|
||||
else:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
multidata = file.read()
|
||||
MultiServer.Context._decompress(multidata)
|
||||
MultiServer.Context.decompress(multidata)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
raise
|
||||
else:
|
||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||
flush() # place into DB and generate ids
|
||||
|
||||
@@ -13,6 +13,10 @@ These steps should be followed in order to establish a gameplay connection with
|
||||
|
||||
In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet.
|
||||
|
||||
There are libraries available that implement the this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) and [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net)
|
||||
|
||||
For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py)
|
||||
|
||||
## Synchronizing Items
|
||||
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
||||
|
||||
@@ -51,13 +55,13 @@ Sent to clients when they connect to an Archipelago server.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
||||
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| password | bool | Denoted whether a password is required to join this room.|
|
||||
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
|
||||
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
|
||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. |
|
||||
| games | list\[str\] | sorted list of game names for the players, so first player's game will be games\[0\]. Matches game names in datapackage. |
|
||||
| datapackage_version | int | Data version of the [data package](#Data-Package-Contents) the server will send. Used to update the client's (optional) local cache. |
|
||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. |
|
||||
@@ -110,7 +114,7 @@ Sent to clients when the connection handshake is successfully completed.
|
||||
| ---- | ---- | ----- |
|
||||
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
|
||||
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
|
||||
| players | list\[NetworkPlayer\] | List denoting other players in the multiworld, whether connected or not. See [NetworkPlayer](#NetworkPlayer) for info on the format. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
||||
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. |
|
||||
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
|
||||
@@ -121,14 +125,14 @@ Sent to clients when they receive an item.
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| index | int | The next empty slot in the list of items for the receiving client. |
|
||||
| items | list\[NetworkItem\] | The items which the client is receiving. See [NetworkItem](#NetworkItem) for more details. |
|
||||
| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. |
|
||||
|
||||
### LocationInfo
|
||||
Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted.
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| locations | list\[NetworkItem\] | Contains list of item(s) in the location(s) scouted. See [NetworkItem](#NetworkItem) for more details. |
|
||||
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
|
||||
|
||||
### RoomUpdate
|
||||
Sent when there is a need to update information about the present game session. Generally useful for async games.
|
||||
@@ -139,9 +143,9 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| hint_points | int | New argument. The client's current hint points. |
|
||||
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. |
|
||||
| checked_locations | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
||||
| missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Changed argument. Always sends all players, whether connected or not. |
|
||||
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
||||
| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
||||
|
||||
All arguments for this packet are optional, only changes are sent.
|
||||
|
||||
@@ -157,10 +161,10 @@ Sent to clients purely to display a message to the player. This packet differs f
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| data | list\[JSONMessagePart\] | See [JSONMessagePart](#JSONMessagePart) for more details on this type. |
|
||||
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
|
||||
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
|
||||
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
|
||||
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
|
||||
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
|
||||
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
|
||||
|
||||
### DataPackage
|
||||
@@ -169,7 +173,7 @@ Sent to clients to provide what is known as a 'data package' which contains info
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) |
|
||||
| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. |
|
||||
|
||||
### Bounced
|
||||
Sent to clients after a client requested this message be sent to them, more info in the Bounce package.
|
||||
@@ -209,7 +213,7 @@ Sent by the client to initiate a connection to an Archipelago game session.
|
||||
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
|
||||
| name | str | The player name for this client. |
|
||||
| uuid | str | Unique identifier for player client. |
|
||||
| version | NetworkVersion | An object representing the Archipelago version this client supports. |
|
||||
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
||||
|
||||
#### Authentication
|
||||
|
||||
@@ -64,7 +64,7 @@ generator:
|
||||
# general weights file, within the stated player_files_path location
|
||||
# gets used if players is higher than the amount of per-player files found to fill remaining slots
|
||||
weights_file_path: "weights.yaml"
|
||||
# Meta file name, within the stated player_files_path location, TODO: re-implement this
|
||||
# Meta file name, within the stated player_files_path location
|
||||
meta_file_path: "meta.yaml"
|
||||
# Create a spoiler file
|
||||
# 0 -> None
|
||||
|
||||
@@ -9,6 +9,7 @@ Utils.local_path.cached_path = file_path
|
||||
from BaseClasses import MultiWorld, CollectionState
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
world: MultiWorld
|
||||
_state_cache = {}
|
||||
|
||||
275
test/general/TestFill.py
Normal file
275
test/general/TestFill.py
Normal file
@@ -0,0 +1,275 @@
|
||||
from typing import NamedTuple, List
|
||||
import unittest
|
||||
from worlds.AutoWorld import World
|
||||
from Fill import FillError, fill_restrictive
|
||||
from BaseClasses import MultiWorld, Region, RegionType, Item, Location
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
|
||||
def generate_multi_world(players: int = 1) -> MultiWorld:
|
||||
multi_world = MultiWorld(players)
|
||||
multi_world.player_name = {}
|
||||
for i in range(players):
|
||||
player_id = i+1
|
||||
world = World(multi_world, player_id)
|
||||
multi_world.game[player_id] = world
|
||||
multi_world.worlds[player_id] = world
|
||||
multi_world.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", RegionType.Generic,
|
||||
"Menu Region Hint", player_id, multi_world)
|
||||
multi_world.regions.append(region)
|
||||
|
||||
multi_world.set_seed()
|
||||
multi_world.set_default_common_options()
|
||||
|
||||
return multi_world
|
||||
|
||||
|
||||
class PlayerDefinition(NamedTuple):
|
||||
id: int
|
||||
menu: Region
|
||||
locations: List[Location]
|
||||
prog_items: List[Item]
|
||||
|
||||
|
||||
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition:
|
||||
menu = multi_world.get_region("Menu", player_id)
|
||||
locations = generate_locations(location_count, player_id, None, menu)
|
||||
prog_items = generate_items(prog_item_count, player_id, True)
|
||||
|
||||
return PlayerDefinition(player_id, menu, locations, prog_items)
|
||||
|
||||
|
||||
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]:
|
||||
locations = []
|
||||
for i in range(count):
|
||||
name = "player" + str(player_id) + "_location" + str(i)
|
||||
location = Location(player_id, name, address, region)
|
||||
locations.append(location)
|
||||
region.locations.append(location)
|
||||
return locations
|
||||
|
||||
|
||||
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
|
||||
items = []
|
||||
for i in range(count):
|
||||
name = "player" + str(player_id) + "_item" + str(i)
|
||||
items.append(Item(name, advancement, code, player_id))
|
||||
return items
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_basic_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
self.assertEqual(loc1.item, item0)
|
||||
self.assertEqual([], player1.locations)
|
||||
self.assertEqual([], player1.prog_items)
|
||||
|
||||
def test_ordered_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[0].name, player1.id) and state.has(items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[0])
|
||||
self.assertEqual(locations[1].item, items[1])
|
||||
|
||||
def test_fill_restrictive_remaining_locations(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(
|
||||
item0.name, player1.id))
|
||||
#forces a swap
|
||||
set_rule(loc2, lambda state: state.has(
|
||||
item0.name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item0)
|
||||
self.assertEqual(loc1.item, item1)
|
||||
self.assertEqual(1, len(player1.locations))
|
||||
self.assertEqual(player1.locations[0], loc2)
|
||||
|
||||
def test_minimal_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.accessibility[player1.id] = 'minimal'
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
# Unnecessary unreachable Item
|
||||
self.assertEqual(locations[1].item, items[0])
|
||||
|
||||
def test_reversed_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item1.name, player1.id))
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
self.assertEqual(loc1.item, item0)
|
||||
|
||||
def test_multi_step_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[2].name, player1.id) and state.has(items[3].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
set_rule(locations[2], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
set_rule(locations[3], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
self.assertEqual(locations[1].item, items[2])
|
||||
self.assertEqual(locations[2].item, items[0])
|
||||
self.assertEqual(locations[3].item, items[3])
|
||||
|
||||
def test_impossible_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
items[0].name, player1.id) and state.has(items[1].name, player1.id)
|
||||
set_rule(locations[1], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
set_rule(locations[0], lambda state: state.has(
|
||||
items[0].name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_circular_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
item2 = player1.prog_items[2]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item0.name, player1.id))
|
||||
set_rule(loc2, lambda state: state.has(item1.name, player1.id))
|
||||
set_rule(loc0, lambda state: state.has(item2.name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_competing_fill_restrictive(self):
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id)
|
||||
set_rule(loc1, lambda state: state.has(item0.name, player1.id)
|
||||
and state.has(item1.name, player1.id))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_multiplayer_fill_restrictive(self):
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
player1.prog_items[0].name, player1.id) and state.has(
|
||||
player1.prog_items[1].name, player1.id)
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id) and state.has(
|
||||
player2.prog_items[1].name, player2.id)
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player1.prog_items[1])
|
||||
self.assertEqual(player1.locations[1].item, player2.prog_items[1])
|
||||
self.assertEqual(player2.locations[0].item, player1.prog_items[0])
|
||||
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
|
||||
|
||||
def test_multiplayer_rules_fill_restrictive(self):
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
player1.prog_items[0].name, player1.id) and state.has(
|
||||
player1.prog_items[1].name, player1.id)
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id) and state.has(
|
||||
player2.prog_items[1].name, player2.id)
|
||||
|
||||
set_rule(player2.locations[1], lambda state: state.has(
|
||||
player2.prog_items[0].name, player2.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player2.prog_items[0])
|
||||
self.assertEqual(player1.locations[1].item, player2.prog_items[1])
|
||||
self.assertEqual(player2.locations[0].item, player1.prog_items[0])
|
||||
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
|
||||
@@ -4,15 +4,15 @@ from BaseClasses import MultiWorld
|
||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||
from worlds.alttp.EntranceShuffle import link_entrances
|
||||
from worlds.alttp.InvertedRegions import mark_dark_world_regions
|
||||
from worlds.alttp.ItemPool import difficulties, generate_itempool
|
||||
from worlds.alttp.ItemPool import difficulties
|
||||
from worlds.alttp.Items import ItemFactory
|
||||
from worlds.alttp.Regions import create_regions
|
||||
from worlds.alttp.Shops import create_shops
|
||||
from worlds.alttp.Rules import set_rules
|
||||
from test.TestBase import TestBase
|
||||
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class TestMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
@@ -30,8 +30,10 @@ class TestMinor(TestBase):
|
||||
self.world.worlds[1].create_items()
|
||||
self.world.required_medallions[1] = ['Ether', 'Quake']
|
||||
self.world.itempool.extend(get_dungeon_item_pool(self.world))
|
||||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||
self.world.itempool.extend(ItemFactory(
|
||||
['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1',
|
||||
'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||
self.world.get_location('Agahnim 1', 1).item = None
|
||||
self.world.get_location('Agahnim 2', 1).item = None
|
||||
mark_dark_world_regions(self.world, 1)
|
||||
self.world.worlds[1].set_rules()
|
||||
self.world.worlds[1].set_rules()
|
||||
|
||||
@@ -2549,7 +2549,7 @@ DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
|
||||
'Big Bomb Shop',
|
||||
'Dark Death Mountain Fairy',
|
||||
'Dark Lake Hylia Shop',
|
||||
'Dark World Shop',
|
||||
'Village of Outcasts Shop',
|
||||
'Red Shield Shop',
|
||||
'Mire Shed',
|
||||
'East Dark World Hint',
|
||||
@@ -2626,7 +2626,7 @@ Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
|
||||
'Red Shield Shop',
|
||||
'Dark Sanctuary Hint',
|
||||
'Fortune Teller (Dark)',
|
||||
'Dark World Shop',
|
||||
'Village of Outcasts Shop',
|
||||
'Dark World Lumberjack Shop',
|
||||
'Dark World Potion Shop',
|
||||
'Archery Game',
|
||||
@@ -2837,7 +2837,7 @@ Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
|
||||
'C-Shaped House',
|
||||
'Bumper Cave (Top)',
|
||||
'Dark Lake Hylia Shop',
|
||||
'Dark World Shop',
|
||||
'Village of Outcasts Shop',
|
||||
'Red Shield Shop',
|
||||
'Mire Shed',
|
||||
'East Dark World Hint',
|
||||
@@ -2883,7 +2883,7 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
|
||||
'Red Shield Shop',
|
||||
'Inverted Dark Sanctuary',
|
||||
'Fortune Teller (Dark)',
|
||||
'Dark World Shop',
|
||||
'Village of Outcasts Shop',
|
||||
'Dark World Lumberjack Shop',
|
||||
'Dark World Potion Shop',
|
||||
'Archery Game',
|
||||
@@ -3543,7 +3543,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'),
|
||||
('Red Shield Shop', 'Red Shield Shop'),
|
||||
('Dark Sanctuary Hint', 'Dark Sanctuary Hint'),
|
||||
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'),
|
||||
('Dark World Shop', 'Village of Outcasts Shop'),
|
||||
('Village of Outcasts Shop', 'Village of Outcasts Shop'),
|
||||
('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'),
|
||||
('Dark World Potion Shop', 'Dark World Potion Shop'),
|
||||
('Archery Game', 'Archery Game'),
|
||||
@@ -3679,7 +3679,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
|
||||
('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'),
|
||||
('Red Shield Shop', 'Red Shield Shop'),
|
||||
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'),
|
||||
('Dark World Shop', 'Village of Outcasts Shop'),
|
||||
('Village of Outcasts Shop', 'Village of Outcasts Shop'),
|
||||
('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'),
|
||||
('Dark World Potion Shop', 'Dark World Potion Shop'),
|
||||
('Archery Game', 'Archery Game'),
|
||||
@@ -3981,7 +3981,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0
|
||||
'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)),
|
||||
'Inverted Dark Sanctuary': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)),
|
||||
'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000)),
|
||||
'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000)),
|
||||
'Village of Outcasts Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000)),
|
||||
'Dark World Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000)),
|
||||
'Dark World Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000)),
|
||||
'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000)),
|
||||
|
||||
@@ -184,7 +184,7 @@ def create_inverted_regions(world, player):
|
||||
create_dw_region(player, 'West Dark World', ['Frog', 'Flute Activation Spot'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop',
|
||||
'West Dark World Teleporter', 'WDW Flute']),
|
||||
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop', 'Dark Grassy Lawn Flute']),
|
||||
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
|
||||
create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
|
||||
create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
|
||||
@@ -9,7 +9,7 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||
from Fill import FillError
|
||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle
|
||||
|
||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
@@ -226,6 +226,7 @@ for diff in {'easy', 'normal', 'hard', 'expert'}:
|
||||
def generate_itempool(world):
|
||||
player = world.player
|
||||
world = world.world
|
||||
|
||||
if world.difficulty[player] not in difficulties:
|
||||
raise NotImplementedError(f"Diffulty {world.difficulty[player]}")
|
||||
if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt',
|
||||
@@ -371,14 +372,27 @@ def generate_itempool(world):
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world, player)
|
||||
if item.name not in world.worlds[player].dungeon_local_item_names]
|
||||
|
||||
dungeon_item_replacements = difficulties[world.difficulty[player]].extras[0]\
|
||||
+ difficulties[world.difficulty[player]].extras[1]\
|
||||
+ difficulties[world.difficulty[player]].extras[2]\
|
||||
+ difficulties[world.difficulty[player]].extras[3]\
|
||||
+ difficulties[world.difficulty[player]].extras[4]
|
||||
world.random.shuffle(dungeon_item_replacements)
|
||||
if world.goal[player] == 'icerodhunt':
|
||||
for item in dungeon_items:
|
||||
world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player))
|
||||
world.push_precollected(item)
|
||||
else:
|
||||
for x in range(len(dungeon_items)-1, -1, -1):
|
||||
item = dungeon_items[x]
|
||||
if ((world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey')
|
||||
or (world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey')
|
||||
or (world.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
|
||||
or (world.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
|
||||
dungeon_items.remove(item)
|
||||
world.push_precollected(item)
|
||||
world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player))
|
||||
world.itempool.extend([item for item in dungeon_items])
|
||||
|
||||
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||
@@ -651,6 +665,7 @@ def get_pool_core(world, player: int):
|
||||
place_item(key_location, item_to_place)
|
||||
else:
|
||||
pool.extend([item_to_place])
|
||||
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
|
||||
additional_pieces_to_place)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class DungeonItem(Choice):
|
||||
option_own_world = 2
|
||||
option_any_world = 3
|
||||
option_different_world = 4
|
||||
option_start_with = 6
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@@ -90,6 +91,11 @@ class ShopItemSlots(Range):
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
|
||||
class ShopPriceModifier(Range):
|
||||
"""Percentage modifier for shuffled item prices in shops"""
|
||||
range_start = 0
|
||||
default = 100
|
||||
range_end = 400
|
||||
|
||||
class WorldState(Choice):
|
||||
option_standard = 1
|
||||
@@ -305,6 +311,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"killable_thieves": KillableThieves,
|
||||
"bush_shuffle": BushShuffle,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
"shop_price_modifier": ShopPriceModifier,
|
||||
"tile_shuffle": TileShuffle,
|
||||
"ow_palettes": OWPalette,
|
||||
"uw_palettes": UWPalette,
|
||||
|
||||
@@ -176,7 +176,7 @@ def create_regions(world, player):
|
||||
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
|
||||
create_dw_region(player, 'West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock',
|
||||
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']),
|
||||
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop']),
|
||||
create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
|
||||
create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
|
||||
create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
|
||||
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'),
|
||||
|
||||
@@ -2800,7 +2800,7 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house',
|
||||
'C-Shaped House': 'The NE house in Village of Outcasts',
|
||||
'Dark Death Mountain Fairy': 'The SW cave on dark DM',
|
||||
'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia',
|
||||
'Dark World Shop': 'The hammer sealed building',
|
||||
'Village of Outcasts Shop': 'The hammer sealed building',
|
||||
'Red Shield Shop': 'The fenced in building',
|
||||
'Mire Shed': 'The western hut in the mire',
|
||||
'East Dark World Hint': 'The dark cave near the eastmost portal',
|
||||
|
||||
@@ -1007,7 +1007,7 @@ def set_big_bomb_rules(world, player):
|
||||
'Red Shield Shop',
|
||||
'Dark Sanctuary Hint',
|
||||
'Fortune Teller (Dark)',
|
||||
'Dark World Shop',
|
||||
'Village of Outcasts Shop',
|
||||
'Dark World Lumberjack Shop',
|
||||
'Thieves Town',
|
||||
'Skull Woods First Section Door',
|
||||
@@ -1331,7 +1331,7 @@ def set_inverted_big_bomb_rules(world, player):
|
||||
elif bombshop_entrance.name in LW_bush_entrances:
|
||||
# These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player))))
|
||||
elif bombshop_entrance.name == 'Dark World Shop':
|
||||
elif bombshop_entrance.name == 'Village of Outcasts Shop':
|
||||
# This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
|
||||
elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
|
||||
|
||||
@@ -247,7 +247,12 @@ def ShopSlotFill(world):
|
||||
|
||||
item_name = location.item.name
|
||||
if location.item.game != "A Link to the Past":
|
||||
price = world.random.randrange(1, 28)
|
||||
if location.item.advancement:
|
||||
price = world.random.randrange(8, 56)
|
||||
elif location.item.never_exclude:
|
||||
price = world.random.randrange(4, 28)
|
||||
else:
|
||||
price = world.random.randrange(2, 14)
|
||||
elif any(x in item_name for x in
|
||||
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
price = world.random.randrange(1, 7)
|
||||
@@ -258,7 +263,8 @@ def ShopSlotFill(world):
|
||||
else:
|
||||
price = world.random.randrange(8, 56)
|
||||
|
||||
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
|
||||
shop.push_inventory(location.shop_slot, item_name,
|
||||
min(int(price * world.shop_price_modifier[location.player] / 100) * 5, 9999), 1,
|
||||
location.item.player if location.item.player != location.player else 0)
|
||||
if 'P' in world.shop_shuffle[location.player]:
|
||||
price_to_funny_price(shop.inventory[location.shop_slot], world, location.player)
|
||||
|
||||
@@ -131,6 +131,8 @@ class RecipeTime(Choice):
|
||||
|
||||
|
||||
class Progressive(Choice):
|
||||
"""Merges together Technologies like "automation-1" to "automation-3" into 3 copies of "Progressive Automation",
|
||||
which awards them in order."""
|
||||
displayname = "Progressive Technologies"
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
@@ -151,17 +153,19 @@ class RecipeIngredients(Choice):
|
||||
|
||||
|
||||
class FactorioStartItems(ItemDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
displayname = "Starting Items"
|
||||
verify_item_name = False
|
||||
default = {"burner-mining-drill": 19, "stone-furnace": 19}
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
"""Set of items that should never be granted from Free Samples"""
|
||||
displayname = "Free Sample Blacklist"
|
||||
|
||||
|
||||
class FactorioFreeSampleWhitelist(OptionSet):
|
||||
"""overrides any free sample blacklist present. This may ruin the balance of the mod, be forewarned."""
|
||||
"""Overrides any free sample blacklist present. This may ruin the balance of the mod, be warned."""
|
||||
displayname = "Free Sample Whitelist"
|
||||
|
||||
|
||||
@@ -180,6 +184,7 @@ class EvolutionTrapCount(TrapCount):
|
||||
|
||||
|
||||
class EvolutionTrapIncrease(Range):
|
||||
"""How much an Evolution Trap increases the enemy evolution"""
|
||||
displayname = "Evolution Trap % Effect"
|
||||
range_start = 1
|
||||
default = 10
|
||||
@@ -187,6 +192,8 @@ class EvolutionTrapIncrease(Range):
|
||||
|
||||
|
||||
class FactorioWorldGen(OptionDict):
|
||||
"""World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator,
|
||||
with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings"""
|
||||
displayname = "World Generation"
|
||||
# FIXME: do we want default be a rando-optimized default or in-game DS?
|
||||
value: typing.Dict[str, typing.Dict[str, typing.Any]]
|
||||
@@ -320,6 +327,7 @@ class FactorioWorldGen(OptionDict):
|
||||
|
||||
|
||||
class ImportedBlueprint(DefaultOnToggle):
|
||||
"""Allow or Disallow Blueprints from outside the current savegame."""
|
||||
displayname = "Blueprints"
|
||||
|
||||
|
||||
|
||||
@@ -68,16 +68,12 @@ class HKWorld(World):
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player)
|
||||
|
||||
def generate_output(self):
|
||||
pass # Hollow Knight needs no output files
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_data = {}
|
||||
for option_name in self.options:
|
||||
|
||||
@@ -312,7 +312,7 @@ def get_hint_area(spot):
|
||||
|
||||
spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances)))
|
||||
|
||||
raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id))
|
||||
raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.player))
|
||||
else:
|
||||
return spot.name
|
||||
|
||||
|
||||
@@ -191,8 +191,8 @@ world_options: typing.Dict[str, type(Option)] = {
|
||||
"owl_drops": OwlDrops,
|
||||
"warp_songs": WarpSongs,
|
||||
"spawn_positions": SpawnPositions,
|
||||
"mix_entrance_pools": MixEntrancePools,
|
||||
"decouple_entrances": DecoupleEntrances,
|
||||
# "mix_entrance_pools": MixEntrancePools,
|
||||
# "decouple_entrances": DecoupleEntrances,
|
||||
"triforce_hunt": TriforceHunt,
|
||||
"triforce_goal": TriforceGoal,
|
||||
"extra_triforce_percentage": ExtraTriforces,
|
||||
|
||||
@@ -282,7 +282,7 @@ class Rom(BigStream):
|
||||
|
||||
|
||||
def compress_rom_file(input_file, output_file):
|
||||
compressor_path = data_path("Compress")
|
||||
compressor_path = "."
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
executable_path = "Compress.exe"
|
||||
|
||||
@@ -140,6 +140,11 @@ def set_rules(ootworld):
|
||||
location = world.get_location('Sheik in Ice Cavern', player)
|
||||
add_item_rule(location, lambda item: item.player == player and item.type == 'Song')
|
||||
|
||||
if ootworld.skip_child_zelda:
|
||||
# If skip child zelda is on, the item at Song from Impa must be giveable by the save context.
|
||||
location = world.get_location('Song from Impa', player)
|
||||
add_item_rule(location, lambda item: item in SaveContext.giveable_items)
|
||||
|
||||
for name in ootworld.always_hints:
|
||||
add_rule(world.get_location(name, player), guarantee_hint)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from .N64Patch import create_patch_file
|
||||
from .Cosmetics import patch_cosmetics
|
||||
from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
|
||||
from .HintList import getRequiredHints
|
||||
from .SaveContext import SaveContext
|
||||
|
||||
from Utils import get_options, output_path
|
||||
from BaseClasses import MultiWorld, CollectionState, RegionType
|
||||
@@ -186,6 +187,8 @@ class OOTWorld(World):
|
||||
self.mq_dungeons_random = False # this will be a deprecated option later
|
||||
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
|
||||
self.big_poe_count = 1 # disabled due to client-side issues for now
|
||||
self.mix_entrance_pools = False
|
||||
self.decouple_entrances = False
|
||||
|
||||
# Set internal names used by the OoT generator
|
||||
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
|
||||
@@ -469,13 +472,16 @@ class OOTWorld(World):
|
||||
self.remove_from_start_inventory.remove(item.name)
|
||||
removed_items.append(item.name)
|
||||
else:
|
||||
self.starting_items[item.name] += 1
|
||||
if item.type == 'Song':
|
||||
self.starting_songs = True
|
||||
# Call the junk fill and get a replacement
|
||||
if item in self.itempool:
|
||||
self.itempool.remove(item)
|
||||
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
|
||||
if item.name not in SaveContext.giveable_items:
|
||||
raise Exception(f"Invalid OoT starting item: {item.name}")
|
||||
else:
|
||||
self.starting_items[item.name] += 1
|
||||
if item.type == 'Song':
|
||||
self.starting_songs = True
|
||||
# Call the junk fill and get a replacement
|
||||
if item in self.itempool:
|
||||
self.itempool.remove(item)
|
||||
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
|
||||
if self.start_with_consumables:
|
||||
self.starting_items['Deku Sticks'] = 30
|
||||
self.starting_items['Deku Nuts'] = 40
|
||||
@@ -716,7 +722,6 @@ class OOTWorld(World):
|
||||
impa = self.world.get_location("Song from Impa", self.player)
|
||||
if self.skip_child_zelda:
|
||||
if impa.item is None:
|
||||
from .SaveContext import SaveContext
|
||||
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
||||
item.player == self.player and item.name in SaveContext.giveable_items))
|
||||
impa.place_locked_item(item_to_place)
|
||||
@@ -827,7 +832,12 @@ class OOTWorld(World):
|
||||
or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])):
|
||||
autoworld.major_item_locations.append(loc)
|
||||
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or loc.item.type == 'Song'):
|
||||
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
|
||||
(loc.item.type == 'Song' or
|
||||
(loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
|
||||
(loc.item.type == 'FortressSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
|
||||
(loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
|
||||
(loc.item.type == 'GanonBossKey' and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
|
||||
if loc.player in barren_hint_players:
|
||||
hint_area = get_hint_area(loc)
|
||||
items_by_region[loc.player][hint_area]['weight'] += 1
|
||||
|
||||
1
worlds/oot/data/Compress/dmaTable.dat
Normal file
1
worlds/oot/data/Compress/dmaTable.dat
Normal file
@@ -0,0 +1 @@
|
||||
0 1 2 3 4 5 6 7 8 9 15 16 17 18 19 20 21 22 23 24 25 26 942 944 946 948 950 952 954 956 958 960 962 964 966 968 970 972 974 976 978 980 982 984 986 988 990 992 994 996 998 1000 1002 1004 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525
|
||||
129
worlds/rogue-legacy/Items.py
Normal file
129
worlds/rogue-legacy/Items.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
code: typing.Optional[int]
|
||||
progression: bool
|
||||
quantity: int = 1
|
||||
event: bool = False
|
||||
|
||||
|
||||
class LegacyItem(Item):
|
||||
game: str = "Rogue Legacy"
|
||||
|
||||
def __init__(self, name, advancement: bool = False, code: int = None, player: int = None):
|
||||
super(LegacyItem, self).__init__(name, advancement, code, player)
|
||||
|
||||
|
||||
# Separate tables for each type of item.
|
||||
vendors_table = {
|
||||
ItemName.blacksmith: ItemData(90000, True),
|
||||
ItemName.enchantress: ItemData(90001, True),
|
||||
ItemName.architect: ItemData(90002, False),
|
||||
}
|
||||
|
||||
static_classes_table = {
|
||||
ItemName.knight: ItemData(90080, True),
|
||||
ItemName.paladin: ItemData(90081, True),
|
||||
ItemName.mage: ItemData(90082, True),
|
||||
ItemName.archmage: ItemData(90083, True),
|
||||
ItemName.barbarian: ItemData(90084, True),
|
||||
ItemName.barbarian_king: ItemData(90085, True),
|
||||
ItemName.knave: ItemData(90086, True),
|
||||
ItemName.assassin: ItemData(90087, True),
|
||||
ItemName.shinobi: ItemData(90088, True),
|
||||
ItemName.hokage: ItemData(90089, True),
|
||||
ItemName.miner: ItemData(90090, True),
|
||||
ItemName.spelunker: ItemData(90091, True),
|
||||
ItemName.lich: ItemData(90092, True),
|
||||
ItemName.lich_king: ItemData(90093, True),
|
||||
ItemName.spellthief: ItemData(90094, True),
|
||||
ItemName.spellsword: ItemData(90095, True),
|
||||
ItemName.dragon: ItemData(90096, True),
|
||||
ItemName.traitor: ItemData(90097, True),
|
||||
}
|
||||
|
||||
progressive_classes_table = {
|
||||
ItemName.progressive_knight: ItemData(90003, True, 2),
|
||||
ItemName.progressive_mage: ItemData(90004, True, 2),
|
||||
ItemName.progressive_barbarian: ItemData(90005, True, 2),
|
||||
ItemName.progressive_knave: ItemData(90006, True, 2),
|
||||
ItemName.progressive_shinobi: ItemData(90007, True, 2),
|
||||
ItemName.progressive_miner: ItemData(90008, True, 2),
|
||||
ItemName.progressive_lich: ItemData(90009, True, 2),
|
||||
ItemName.progressive_spellthief: ItemData(90010, True, 2),
|
||||
}
|
||||
|
||||
skill_unlocks_table = {
|
||||
ItemName.health: ItemData(90013, True, 15),
|
||||
ItemName.mana: ItemData(90014, True, 15),
|
||||
ItemName.attack: ItemData(90015, True, 15),
|
||||
ItemName.magic_damage: ItemData(90016, True, 15),
|
||||
ItemName.armor: ItemData(90017, True, 10),
|
||||
ItemName.equip: ItemData(90018, True, 10),
|
||||
ItemName.crit_chance: ItemData(90019, False, 5),
|
||||
ItemName.crit_damage: ItemData(90020, False, 5),
|
||||
ItemName.down_strike: ItemData(90021, False),
|
||||
ItemName.gold_gain: ItemData(90022, False),
|
||||
ItemName.potion_efficiency: ItemData(90023, False),
|
||||
ItemName.invulnerability_time: ItemData(90024, False),
|
||||
ItemName.mana_cost_down: ItemData(90025, False),
|
||||
ItemName.death_defiance: ItemData(90026, False),
|
||||
ItemName.haggling: ItemData(90027, False),
|
||||
ItemName.random_children: ItemData(90028, False),
|
||||
}
|
||||
|
||||
blueprints_table = {
|
||||
ItemName.squire_blueprints: ItemData(90040, True),
|
||||
ItemName.silver_blueprints: ItemData(90041, True),
|
||||
ItemName.guardian_blueprints: ItemData(90042, True),
|
||||
ItemName.imperial_blueprints: ItemData(90043, True),
|
||||
ItemName.royal_blueprints: ItemData(90044, True),
|
||||
ItemName.knight_blueprints: ItemData(90045, True),
|
||||
ItemName.ranger_blueprints: ItemData(90046, True),
|
||||
ItemName.sky_blueprints: ItemData(90047, True),
|
||||
ItemName.dragon_blueprints: ItemData(90048, True),
|
||||
ItemName.slayer_blueprints: ItemData(90049, True),
|
||||
ItemName.blood_blueprints: ItemData(90050, True),
|
||||
ItemName.sage_blueprints: ItemData(90051, True),
|
||||
ItemName.retribution_blueprints: ItemData(90052, True),
|
||||
ItemName.holy_blueprints: ItemData(90053, True),
|
||||
ItemName.dark_blueprints: ItemData(90054, True),
|
||||
}
|
||||
|
||||
runes_table = {
|
||||
ItemName.vault_runes: ItemData(90060, True),
|
||||
ItemName.sprint_runes: ItemData(90061, True),
|
||||
ItemName.vampire_runes: ItemData(90062, True),
|
||||
ItemName.sky_runes: ItemData(90063, True),
|
||||
ItemName.siphon_runes: ItemData(90064, True),
|
||||
ItemName.retaliation_runes: ItemData(90065, True),
|
||||
ItemName.bounty_runes: ItemData(90066, True),
|
||||
ItemName.haste_runes: ItemData(90067, True),
|
||||
ItemName.curse_runes: ItemData(90068, True),
|
||||
ItemName.grace_runes: ItemData(90069, True),
|
||||
ItemName.balance_runes: ItemData(90070, True),
|
||||
}
|
||||
|
||||
misc_items_table = {
|
||||
ItemName.trip_stat_increase: ItemData(90030, False),
|
||||
ItemName.gold_1000: ItemData(90031, False),
|
||||
ItemName.gold_3000: ItemData(90032, False),
|
||||
ItemName.gold_5000: ItemData(90033, False),
|
||||
}
|
||||
|
||||
# Complete item table.
|
||||
item_table = {
|
||||
**vendors_table,
|
||||
**static_classes_table,
|
||||
**progressive_classes_table,
|
||||
**skill_unlocks_table,
|
||||
**blueprints_table,
|
||||
**runes_table,
|
||||
**misc_items_table,
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||
85
worlds/rogue-legacy/Locations.py
Normal file
85
worlds/rogue-legacy/Locations.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Names import LocationName
|
||||
|
||||
|
||||
class LegacyLocation(Location):
|
||||
game: str = "Rogue Legacy"
|
||||
|
||||
|
||||
base_location_table = {
|
||||
# Manor Renovations
|
||||
LocationName.manor_ground_base: 91000,
|
||||
LocationName.manor_main_base: 91001,
|
||||
LocationName.manor_main_bottom_window: 91002,
|
||||
LocationName.manor_main_top_window: 91003,
|
||||
LocationName.manor_main_roof: 91004,
|
||||
LocationName.manor_left_wing_base: 91005,
|
||||
LocationName.manor_left_wing_window: 91006,
|
||||
LocationName.manor_left_wing_roof: 91007,
|
||||
LocationName.manor_left_big_base: 91008,
|
||||
LocationName.manor_left_big_upper1: 91009,
|
||||
LocationName.manor_left_big_upper2: 91010,
|
||||
LocationName.manor_left_big_windows: 91011,
|
||||
LocationName.manor_left_big_roof: 91012,
|
||||
LocationName.manor_left_far_base: 91013,
|
||||
LocationName.manor_left_far_roof: 91014,
|
||||
LocationName.manor_left_extension: 91015,
|
||||
LocationName.manor_left_tree1: 91016,
|
||||
LocationName.manor_left_tree2: 91017,
|
||||
LocationName.manor_right_wing_base: 91018,
|
||||
LocationName.manor_right_wing_window: 91019,
|
||||
LocationName.manor_right_wing_roof: 91020,
|
||||
LocationName.manor_right_big_base: 91021,
|
||||
LocationName.manor_right_big_upper: 91022,
|
||||
LocationName.manor_right_big_roof: 91023,
|
||||
LocationName.manor_right_high_base: 91024,
|
||||
LocationName.manor_right_high_upper: 91025,
|
||||
LocationName.manor_right_high_tower: 91026,
|
||||
LocationName.manor_right_extension: 91027,
|
||||
LocationName.manor_right_tree: 91028,
|
||||
LocationName.manor_observatory_base: 91029,
|
||||
LocationName.manor_observatory_scope: 91030,
|
||||
|
||||
# Boss Rewards
|
||||
LocationName.boss_khindr: 91100,
|
||||
LocationName.boss_alexander: 91102,
|
||||
LocationName.boss_leon: 91104,
|
||||
LocationName.boss_herodotus: 91106,
|
||||
|
||||
# Special Rooms
|
||||
LocationName.special_jukebox: 91200,
|
||||
|
||||
# Special Locations
|
||||
LocationName.castle: None,
|
||||
LocationName.garden: None,
|
||||
LocationName.tower: None,
|
||||
LocationName.dungeon: None,
|
||||
LocationName.fountain: None,
|
||||
}
|
||||
|
||||
diary_location_table = {f"{LocationName.diary} {i + 1}": i + 91300 for i in range(0, 25)}
|
||||
|
||||
fairy_chest_location_table = {
|
||||
**{f"{LocationName.castle} - Fairy Chest {i + 1}": i + 91400 for i in range(0, 50)},
|
||||
**{f"{LocationName.garden} - Fairy Chest {i + 1}": i + 91450 for i in range(0, 50)},
|
||||
**{f"{LocationName.tower} - Fairy Chest {i + 1}": i + 91500 for i in range(0, 50)},
|
||||
**{f"{LocationName.dungeon} - Fairy Chest {i + 1}": i + 91550 for i in range(0, 50)},
|
||||
}
|
||||
|
||||
chest_location_table = {
|
||||
**{f"{LocationName.castle} - Chest {i + 1}": i + 91600 for i in range(0, 100)},
|
||||
**{f"{LocationName.garden} - Chest {i + 1}": i + 91700 for i in range(0, 100)},
|
||||
**{f"{LocationName.tower} - Chest {i + 1}": i + 91800 for i in range(0, 100)},
|
||||
**{f"{LocationName.dungeon} - Chest {i + 1}": i + 91900 for i in range(0, 100)},
|
||||
}
|
||||
|
||||
location_table = {
|
||||
**base_location_table,
|
||||
**diary_location_table,
|
||||
**fairy_chest_location_table,
|
||||
**chest_location_table,
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in location_table.items()}
|
||||
95
worlds/rogue-legacy/Names/ItemName.py
Normal file
95
worlds/rogue-legacy/Names/ItemName.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# Vendor Definitions
|
||||
blacksmith = "Blacksmith"
|
||||
enchantress = "Enchantress"
|
||||
architect = "Architect"
|
||||
|
||||
# Progressive Class Definitions
|
||||
progressive_knight = "Progressive Knights"
|
||||
progressive_mage = "Progressive Mages"
|
||||
progressive_barbarian = "Progressive Barbarians"
|
||||
progressive_knave = "Progressive Knaves"
|
||||
progressive_shinobi = "Progressive Shinobis"
|
||||
progressive_miner = "Progressive Miners"
|
||||
progressive_lich = "Progressive Liches"
|
||||
progressive_spellthief = "Progressive Spellthieves"
|
||||
|
||||
# Static Class Definitions
|
||||
knight = "Knights"
|
||||
paladin = "Paladins"
|
||||
mage = "Mages"
|
||||
archmage = "Archmages"
|
||||
barbarian = "Barbarians"
|
||||
barbarian_king = "Barbarian Kings"
|
||||
knave = "Knaves"
|
||||
assassin = "Assassins"
|
||||
shinobi = "Shinobis"
|
||||
hokage = "Hokages"
|
||||
miner = "Miners"
|
||||
spelunker = "Spelunkers"
|
||||
lich = "Lichs"
|
||||
lich_king = "Lich Kings"
|
||||
spellthief = "Spellthieves"
|
||||
spellsword = "Spellswords"
|
||||
dragon = "Dragons"
|
||||
traitor = "Traitors"
|
||||
|
||||
# Skill Unlock Definitions
|
||||
health = "Health Up"
|
||||
mana = "Mana Up"
|
||||
attack = "Attack Up"
|
||||
magic_damage = "Magic Damage Up"
|
||||
armor = "Armor Up"
|
||||
equip = "Equip Up"
|
||||
crit_chance = "Crit Chance Up"
|
||||
crit_damage = "Crit Damage Up"
|
||||
down_strike = "Down Strike Up"
|
||||
gold_gain = "Gold Gain Up"
|
||||
potion_efficiency = "Potion Efficiency Up"
|
||||
invulnerability_time = "Invulnerability Time Up"
|
||||
mana_cost_down = "Mana Cost Down"
|
||||
death_defiance = "Death Defiance"
|
||||
haggling = "Haggling"
|
||||
random_children = "Randomize Children"
|
||||
|
||||
# Misc. Definitions
|
||||
trip_stat_increase = "Triple Stat Increase"
|
||||
gold_1000 = "1000 Gold"
|
||||
gold_3000 = "3000 Gold"
|
||||
gold_5000 = "5000 Gold"
|
||||
|
||||
# Blueprint Definitions
|
||||
squire_blueprints = "Squire Armor Blueprints"
|
||||
silver_blueprints = "Silver Armor Blueprints"
|
||||
guardian_blueprints = "Guardian Armor Blueprints"
|
||||
imperial_blueprints = "Imperial Armor Blueprints"
|
||||
royal_blueprints = "Royal Armor Blueprints"
|
||||
knight_blueprints = "Knight Armor Blueprints"
|
||||
ranger_blueprints = "Ranger Armor Blueprints"
|
||||
sky_blueprints = "Sky Armor Blueprints"
|
||||
dragon_blueprints = "Dragon Armor Blueprints"
|
||||
slayer_blueprints = "Slayer Armor Blueprints"
|
||||
blood_blueprints = "Blood Armor Blueprints"
|
||||
sage_blueprints = "Sage Armor Blueprints"
|
||||
retribution_blueprints = "Retribution Armor Blueprints"
|
||||
holy_blueprints = "Holy Armor Blueprints"
|
||||
dark_blueprints = "Dark Armor Blueprints"
|
||||
|
||||
# Rune Definitions
|
||||
vault_runes = "Vault Runes"
|
||||
sprint_runes = "Sprint Runes"
|
||||
vampire_runes = "Vampire Runes"
|
||||
sky_runes = "Sky Runes"
|
||||
siphon_runes = "Siphon Runes"
|
||||
retaliation_runes = "Retaliation Runes"
|
||||
bounty_runes = "Bounty Runes"
|
||||
haste_runes = "Haste Runes"
|
||||
curse_runes = "Curse Runes"
|
||||
grace_runes = "Grace Runes"
|
||||
balance_runes = "Balance Runes"
|
||||
|
||||
# Event Definitions
|
||||
boss_khindr = "Defeat Khindr"
|
||||
boss_alexander = "Defeat Alexander"
|
||||
boss_leon = "Defeat Ponce de Leon"
|
||||
boss_herodotus = "Defeat Herodotus"
|
||||
boss_fountain = "Defeat The Fountain"
|
||||
52
worlds/rogue-legacy/Names/LocationName.py
Normal file
52
worlds/rogue-legacy/Names/LocationName.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Manor Piece Definitions
|
||||
manor_ground_base = "Manor Renovation - Ground Road"
|
||||
manor_main_base = "Manor Renovation - Main Base"
|
||||
manor_main_bottom_window = "Manor Renovation - Main Bottom Window"
|
||||
manor_main_top_window = "Manor Renovation - Main Top Window"
|
||||
manor_main_roof = "Manor Renovation - Main Rooftop"
|
||||
manor_left_wing_base = "Manor Renovation - Left Wing Base"
|
||||
manor_left_wing_window = "Manor Renovation - Left Wing Window"
|
||||
manor_left_wing_roof = "Manor Renovation - Left Wing Rooftop"
|
||||
manor_left_big_base = "Manor Renovation - Left Big Base"
|
||||
manor_left_big_upper1 = "Manor Renovation - Left Big Upper 1"
|
||||
manor_left_big_upper2 = "Manor Renovation - Left Big Upper 2"
|
||||
manor_left_big_windows = "Manor Renovation - Left Big Windows"
|
||||
manor_left_big_roof = "Manor Renovation - Left Big Rooftop"
|
||||
manor_left_far_base = "Manor Renovation - Left Far Base"
|
||||
manor_left_far_roof = "Manor Renovation - Left Far Roof"
|
||||
manor_left_extension = "Manor Renovation - Left Extension"
|
||||
manor_left_tree1 = "Manor Renovation - Left Tree 1"
|
||||
manor_left_tree2 = "Manor Renovation - Left Tree 2"
|
||||
manor_right_wing_base = "Manor Renovation - Right Wing Base"
|
||||
manor_right_wing_window = "Manor Renovation - Right Wing Window"
|
||||
manor_right_wing_roof = "Manor Renovation - Right Wing Rooftop"
|
||||
manor_right_big_base = "Manor Renovation - Right Big Base"
|
||||
manor_right_big_upper = "Manor Renovation - Right Big Upper"
|
||||
manor_right_big_roof = "Manor Renovation - Right Big Rooftop"
|
||||
manor_right_high_base = "Manor Renovation - Right High Base"
|
||||
manor_right_high_upper = "Manor Renovation - Right High Upper"
|
||||
manor_right_high_tower = "Manor Renovation - Right High Tower"
|
||||
manor_right_extension = "Manor Renovation - Right Extension"
|
||||
manor_right_tree = "Manor Renovation - Right Tree"
|
||||
manor_observatory_base = "Manor Renovation - Observatory Base"
|
||||
manor_observatory_scope = "Manor Renovation - Observatory Telescope"
|
||||
|
||||
# Boss Chest Definitions
|
||||
boss_khindr = "Khindr's Boss Chest"
|
||||
boss_alexander = "Alexander's Boss Chest"
|
||||
boss_leon = "Ponce de Leon's Boss Chest"
|
||||
boss_herodotus = "Herodotus's Boss Chest"
|
||||
|
||||
# Special Room Definitions
|
||||
special_jukebox = "Jukebox"
|
||||
|
||||
# Shorthand Definitions
|
||||
diary = "Diary"
|
||||
|
||||
# Region Definitions
|
||||
outside = "Outside Castle Hamson"
|
||||
castle = "Castle Hamson"
|
||||
garden = "Forest Abkhazia"
|
||||
tower = "The Maya"
|
||||
dungeon = "The Land of Darkness"
|
||||
fountain = "Fountain Room"
|
||||
128
worlds/rogue-legacy/Options.py
Normal file
128
worlds/rogue-legacy/Options.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import typing
|
||||
|
||||
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle
|
||||
|
||||
|
||||
class StartingGender(Choice):
|
||||
"""
|
||||
Determines the gender of your initial 'Sir Lee' character.
|
||||
"""
|
||||
displayname = "Starting Gender"
|
||||
option_sir = 0
|
||||
option_lady = 1
|
||||
alias_male = 0
|
||||
alias_female = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class StartingClass(Choice):
|
||||
"""
|
||||
Determines the starting class of your initial 'Sir Lee' character.
|
||||
"""
|
||||
displayname = "Starting Class"
|
||||
option_knight = 0
|
||||
option_mage = 1
|
||||
option_barbarian = 2
|
||||
option_knave = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class NewGamePlus(Choice):
|
||||
"""
|
||||
Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not
|
||||
recommended for those inexperienced to Rogue Legacy!
|
||||
"""
|
||||
displayname = "New Game Plus"
|
||||
option_normal = 0
|
||||
option_new_game_plus = 1
|
||||
option_new_game_plus_2 = 2
|
||||
alias_hard = 1
|
||||
alias_brutal = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class FairyChestsPerZone(Range):
|
||||
"""
|
||||
Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat
|
||||
bonuses can be found in Fairy Chests.
|
||||
"""
|
||||
displayname = "Fairy Chests Per Zone"
|
||||
range_start = 5
|
||||
range_end = 15
|
||||
default = 5
|
||||
|
||||
|
||||
class ChestsPerZone(Range):
|
||||
"""
|
||||
Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only
|
||||
gold or stat bonuses can be found in Chests.
|
||||
"""
|
||||
displayname = "Chests Per Zone"
|
||||
range_start = 15
|
||||
range_end = 30
|
||||
default = 15
|
||||
|
||||
|
||||
class Vendors(Choice):
|
||||
"""
|
||||
Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked).
|
||||
"""
|
||||
displayname = "Vendors"
|
||||
option_start_unlocked = 0
|
||||
option_early = 1
|
||||
option_normal = 2
|
||||
option_anywhere = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class DisableCharon(Toggle):
|
||||
"""
|
||||
Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool.
|
||||
"""
|
||||
displayname = "Disable Charon"
|
||||
|
||||
|
||||
class RequirePurchasing(DefaultOnToggle):
|
||||
"""
|
||||
Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before
|
||||
equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account.
|
||||
"""
|
||||
displayname = "Require Purchasing"
|
||||
|
||||
|
||||
class GoldGainMultiplier(Choice):
|
||||
"""
|
||||
Adjusts the multiplier for gaining gold from all sources.
|
||||
"""
|
||||
displayname = "Gold Gain Multiplier"
|
||||
option_normal = 0
|
||||
option_quarter = 1
|
||||
option_half = 2
|
||||
option_double = 3
|
||||
option_quadruple = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class NumberOfChildren(Range):
|
||||
"""
|
||||
Determines the number of offspring you can choose from on the lineage screen after a death.
|
||||
"""
|
||||
displayname = "Number of Children"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 3
|
||||
|
||||
|
||||
legacy_options: typing.Dict[str, type(Option)] = {
|
||||
"starting_gender": StartingGender,
|
||||
"starting_class": StartingClass,
|
||||
"new_game_plus": NewGamePlus,
|
||||
"fairy_chests_per_zone": FairyChestsPerZone,
|
||||
"chests_per_zone": ChestsPerZone,
|
||||
"vendors": Vendors,
|
||||
"disable_charon": DisableCharon,
|
||||
"require_purchasing": RequirePurchasing,
|
||||
"gold_gain_multiplier": GoldGainMultiplier,
|
||||
"number_of_children": NumberOfChildren,
|
||||
"death_link": DeathLink,
|
||||
}
|
||||
60
worlds/rogue-legacy/Regions.py
Normal file
60
worlds/rogue-legacy/Regions.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance
|
||||
from .Items import LegacyItem
|
||||
from .Locations import LegacyLocation, diary_location_table, location_table, base_location_table
|
||||
from .Names import LocationName, ItemName
|
||||
|
||||
|
||||
def create_regions(world, player: int):
|
||||
|
||||
locations: typing.List[str] = []
|
||||
|
||||
# Add required locations.
|
||||
locations += [location for location in base_location_table]
|
||||
locations += [location for location in diary_location_table]
|
||||
|
||||
# Add chests per settings.
|
||||
fairies = int(world.fairy_chests_per_zone[player])
|
||||
for i in range(0, fairies):
|
||||
locations += [f"{LocationName.castle} - Fairy Chest {i + 1}"]
|
||||
locations += [f"{LocationName.garden} - Fairy Chest {i + 1}"]
|
||||
locations += [f"{LocationName.tower} - Fairy Chest {i + 1}"]
|
||||
locations += [f"{LocationName.dungeon} - Fairy Chest {i + 1}"]
|
||||
|
||||
chests = int(world.chests_per_zone[player])
|
||||
for i in range(0, chests):
|
||||
locations += [f"{LocationName.castle} - Chest {i + 1}"]
|
||||
locations += [f"{LocationName.garden} - Chest {i + 1}"]
|
||||
locations += [f"{LocationName.tower} - Chest {i + 1}"]
|
||||
locations += [f"{LocationName.dungeon} - Chest {i + 1}"]
|
||||
|
||||
# Set up the regions correctly.
|
||||
world.regions += [
|
||||
create_region(world, player, "Menu", None, [LocationName.outside]),
|
||||
create_region(world, player, LocationName.castle, locations),
|
||||
]
|
||||
|
||||
# Connect entrances and set up events.
|
||||
world.get_entrance(LocationName.outside, player).connect(world.get_region(LocationName.castle, player))
|
||||
world.get_location(LocationName.castle, player).place_locked_item(LegacyItem(ItemName.boss_khindr, True, None, player))
|
||||
world.get_location(LocationName.garden, player).place_locked_item(LegacyItem(ItemName.boss_alexander, True, None, player))
|
||||
world.get_location(LocationName.tower, player).place_locked_item(LegacyItem(ItemName.boss_leon, True, None, player))
|
||||
world.get_location(LocationName.dungeon, player).place_locked_item(LegacyItem(ItemName.boss_herodotus, True, None, player))
|
||||
world.get_location(LocationName.fountain, player).place_locked_item(LegacyItem(ItemName.boss_fountain, True, None, player))
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||
# Shamelessly stolen from the ROR2 definition, lol
|
||||
ret = Region(name, None, name, player)
|
||||
ret.world = world
|
||||
if locations:
|
||||
for location in locations:
|
||||
loc_id = location_table.get(location, 0)
|
||||
location = LegacyLocation(player, location, loc_id, ret)
|
||||
ret.locations.append(location)
|
||||
if exits:
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
|
||||
return ret
|
||||
131
worlds/rogue-legacy/Rules.py
Normal file
131
worlds/rogue-legacy/Rules.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from .Names import LocationName, ItemName
|
||||
from ..AutoWorld import LogicMixin
|
||||
from ..generic.Rules import set_rule
|
||||
|
||||
|
||||
class LegacyLogic(LogicMixin):
|
||||
def _legacy_has_any_vendors(self, player: int) -> bool:
|
||||
return self.has_any({ItemName.blacksmith, ItemName.enchantress}, player)
|
||||
|
||||
def _legacy_has_all_vendors(self, player: int) -> bool:
|
||||
return self.has_all({ItemName.blacksmith, ItemName.enchantress}, player)
|
||||
|
||||
def _legacy_has_stat_upgrades(self, player: int, amount: int) -> bool:
|
||||
count: int = self.item_count(ItemName.health, player) + self.item_count(ItemName.mana, player) + \
|
||||
self.item_count(ItemName.attack, player) + self.item_count(ItemName.magic_damage, player) + \
|
||||
self.item_count(ItemName.armor, player) + self.item_count(ItemName.equip, player)
|
||||
return count >= amount
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
# Chests
|
||||
for i in range(0, world.chests_per_zone[player]):
|
||||
set_rule(world.get_location(f"{LocationName.garden} - Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(f"{LocationName.tower} - Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(f"{LocationName.dungeon} - Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
|
||||
# Fairy Chests
|
||||
for i in range(0, world.fairy_chests_per_zone[player]):
|
||||
set_rule(world.get_location(f"{LocationName.garden} - Fairy Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(f"{LocationName.tower} - Fairy Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(f"{LocationName.dungeon} - Fairy Chest {i + 1}", player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
|
||||
# Vendors
|
||||
if world.vendors[player] == "early":
|
||||
set_rule(world.get_location(LocationName.castle, player),
|
||||
lambda state: state._legacy_has_all_vendors(player))
|
||||
elif world.vendors[player] == "normal":
|
||||
set_rule(world.get_location(LocationName.garden, player),
|
||||
lambda state: state._legacy_has_any_vendors(player))
|
||||
elif world.vendors[player] == "anywhere":
|
||||
pass # it can be anywhere, so no rule for this!
|
||||
|
||||
# Diaries
|
||||
for i in range(0, 5):
|
||||
set_rule(world.get_location(f"Diary {i + 6}", player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(f"Diary {i + 11}", player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(f"Diary {i + 16}", player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(f"Diary {i + 21}", player),
|
||||
lambda state: state.has(ItemName.boss_herodotus, player))
|
||||
|
||||
# Scale each manor location.
|
||||
set_rule(world.get_location(LocationName.manor_left_wing_window, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_wing_roof, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_wing_window, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_wing_roof, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_big_base, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_big_base, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_tree1, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_tree2, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_tree, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_big_upper1, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_big_upper2, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_big_windows, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_big_roof, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_far_base, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_far_roof, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_left_extension, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_big_upper, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_big_roof, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_extension, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_high_base, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_high_upper, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(LocationName.manor_right_high_tower, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(LocationName.manor_observatory_base, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(LocationName.manor_observatory_scope, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
|
||||
# Standard Zone Progression
|
||||
set_rule(world.get_location(LocationName.garden, player),
|
||||
lambda state: state._legacy_has_stat_upgrades(player, 10) and state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.tower, player),
|
||||
lambda state: state._legacy_has_stat_upgrades(player, 25) and state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.dungeon, player),
|
||||
lambda state: state._legacy_has_stat_upgrades(player, 40) and state.has(ItemName.boss_leon, player))
|
||||
|
||||
# Bosses
|
||||
set_rule(world.get_location(LocationName.boss_khindr, player),
|
||||
lambda state: state.has(ItemName.boss_khindr, player))
|
||||
set_rule(world.get_location(LocationName.boss_alexander, player),
|
||||
lambda state: state.has(ItemName.boss_alexander, player))
|
||||
set_rule(world.get_location(LocationName.boss_leon, player),
|
||||
lambda state: state.has(ItemName.boss_leon, player))
|
||||
set_rule(world.get_location(LocationName.boss_herodotus, player),
|
||||
lambda state: state.has(ItemName.boss_herodotus, player))
|
||||
set_rule(world.get_location(LocationName.fountain, player),
|
||||
lambda state: state._legacy_has_stat_upgrades(player, 50) and state.has(ItemName.boss_herodotus, player))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has(ItemName.boss_fountain, player)
|
||||
105
worlds/rogue-legacy/__init__.py
Normal file
105
worlds/rogue-legacy/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, MultiWorld
|
||||
from .Items import LegacyItem, ItemData, item_table, vendors_table, static_classes_table, progressive_classes_table, \
|
||||
skill_unlocks_table, blueprints_table, runes_table, misc_items_table
|
||||
from .Locations import LegacyLocation, location_table, base_location_table
|
||||
from .Options import legacy_options
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from .Names import ItemName
|
||||
from ..AutoWorld import World
|
||||
|
||||
|
||||
class LegacyWorld(World):
|
||||
"""
|
||||
Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed
|
||||
you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf.
|
||||
But that's OK, because no one is perfect, and you don't have to be to succeed.
|
||||
"""
|
||||
game: str = "Rogue Legacy"
|
||||
options = legacy_options
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = location_table
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
"starting_gender": self.world.starting_gender[self.player],
|
||||
"starting_class": self.world.starting_class[self.player],
|
||||
"new_game_plus": self.world.new_game_plus[self.player],
|
||||
"fairy_chests_per_zone": self.world.fairy_chests_per_zone[self.player],
|
||||
"chests_per_zone": self.world.chests_per_zone[self.player],
|
||||
"vendors": self.world.vendors[self.player],
|
||||
"disable_charon": self.world.disable_charon[self.player],
|
||||
"require_purchasing": self.world.require_purchasing[self.player],
|
||||
"gold_gain_multiplier": self.world.gold_gain_multiplier[self.player],
|
||||
"number_of_children": self.world.number_of_children[self.player],
|
||||
"death_link": self.world.death_link[self.player],
|
||||
}
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = item_table[name]
|
||||
return [self.create_item(name)] * data.quantity
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_slot_data()
|
||||
for option_name in legacy_options:
|
||||
option = getattr(self.world, option_name)[self.player]
|
||||
slot_data[option_name] = option.value
|
||||
|
||||
return slot_data
|
||||
|
||||
def generate_basic(self):
|
||||
itempool: typing.List[LegacyItem] = []
|
||||
total_required_locations = 61 + (self.world.chests_per_zone[self.player] * 4) + (self.world.fairy_chests_per_zone[self.player] * 4)
|
||||
|
||||
# Fill item pool with all required items
|
||||
for item in {**skill_unlocks_table, **blueprints_table, **runes_table}:
|
||||
# if Haggling, do not add if Disable Charon.
|
||||
if item == ItemName.haggling and self.world.disable_charon[self.player] == 1:
|
||||
continue
|
||||
itempool += self._create_items(item)
|
||||
|
||||
# Add specific classes into the pool. Eventually, will be able to shuffle the starting ones, but until then...
|
||||
itempool += [
|
||||
self.create_item(ItemName.paladin),
|
||||
self.create_item(ItemName.archmage),
|
||||
self.create_item(ItemName.barbarian_king),
|
||||
self.create_item(ItemName.assassin),
|
||||
self.create_item(ItemName.dragon),
|
||||
self.create_item(ItemName.traitor),
|
||||
*self._create_items(ItemName.progressive_shinobi),
|
||||
*self._create_items(ItemName.progressive_miner),
|
||||
*self._create_items(ItemName.progressive_lich),
|
||||
*self._create_items(ItemName.progressive_spellthief),
|
||||
]
|
||||
|
||||
# Check if we need to start with these vendors or put them in the pool.
|
||||
if self.world.vendors[self.player] == "start_unlocked":
|
||||
self.world.push_precollected(self.world.create_item(ItemName.blacksmith, self.player))
|
||||
self.world.push_precollected(self.world.create_item(ItemName.enchantress, self.player))
|
||||
else:
|
||||
itempool += [self.create_item(ItemName.blacksmith), self.create_item(ItemName.enchantress)]
|
||||
|
||||
# Add Arcitect.
|
||||
itempool += [self.create_item(ItemName.architect)]
|
||||
|
||||
# Fill item pool with the remaining
|
||||
for _ in range(len(itempool), total_required_locations):
|
||||
item = self.world.random.choice(list(misc_items_table.keys()))
|
||||
itempool += [self.create_item(item)]
|
||||
|
||||
self.world.itempool += itempool
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = item_table[name]
|
||||
return LegacyItem(name, data.progression, data.code, self.player)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world, self.player)
|
||||
@@ -44,6 +44,7 @@ class SMWorld(World):
|
||||
itemManager: ItemManager
|
||||
|
||||
locations = {}
|
||||
hint_blacklist = {'Nothing', 'NoEnergy'}
|
||||
|
||||
Logic.factory('vanilla')
|
||||
|
||||
@@ -85,6 +86,7 @@ class SMWorld(World):
|
||||
# keeps Nothing items local so no player will ever pickup Nothing
|
||||
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
|
||||
self.world.local_items[self.player].value.add('Nothing')
|
||||
self.world.local_items[self.player].value.add('NoEnergy')
|
||||
|
||||
if (self.variaRando.args.morphPlacement == "early"):
|
||||
self.world.local_items[self.player].value.add('Morph')
|
||||
@@ -126,7 +128,7 @@ class SMWorld(World):
|
||||
weaponCount[2] += 1
|
||||
else:
|
||||
isAdvancement = False
|
||||
elif item.Type == 'Nothing':
|
||||
elif item.Category == 'Nothing':
|
||||
isAdvancement = False
|
||||
|
||||
itemClass = ItemManager.Items[item.Type].Class
|
||||
|
||||
@@ -16,6 +16,7 @@ from utils.doorsmanager import DoorsManager
|
||||
from logic.logic import Logic
|
||||
|
||||
import utils.log
|
||||
from worlds.sm.Options import StartLocation
|
||||
|
||||
# we need to know the logic before doing anything else
|
||||
def getLogic():
|
||||
@@ -498,10 +499,12 @@ class VariaRandomizer:
|
||||
sys.exit(-1)
|
||||
args.startLocation = random.choice(possibleStartAPs)
|
||||
elif args.startLocation not in possibleStartAPs:
|
||||
optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
|
||||
optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
|
||||
dumpErrorMsgs(args.output, optErrMsgs)
|
||||
sys.exit(-1)
|
||||
args.startLocation = 'Landing Site'
|
||||
world.start_location[player] = StartLocation(StartLocation.default)
|
||||
#optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
|
||||
#optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
|
||||
#dumpErrorMsgs(args.output, optErrMsgs)
|
||||
#sys.exit(-1)
|
||||
ap = getAccessPoint(args.startLocation)
|
||||
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
|
||||
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
|
||||
|
||||
@@ -11,6 +11,9 @@ class LocationData(NamedTuple):
|
||||
rule: Callable = lambda state: True
|
||||
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||
# 1337000 - 1337155 Generic locations
|
||||
# 1337171 - 1337175 New Pickup checks
|
||||
# 1337246 - 1337249 Ancient Pyramid
|
||||
location_table: List[LocationData] = [
|
||||
# PresentItemLocations
|
||||
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
||||
@@ -73,12 +76,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData('Sealed Caves (Sirens)', 'Upper sealed cave after sirens chest 1', 1337057),
|
||||
LocationData('Military Fortress', 'Military bomber chest', 1337058, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)),
|
||||
LocationData('Military Fortress', 'Close combat room', 1337059),
|
||||
LocationData('Military Fortress', 'Military soldiers bridge', 1337060),
|
||||
LocationData('Military Fortress', 'Military giantess room', 1337061),
|
||||
LocationData('Military Fortress', 'Military giantess bridge', 1337062),
|
||||
LocationData('Military Fortress', 'Military B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('Military Fortress', 'Military B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('Military Fortress', 'Military pedestal', 1337065, lambda state: state._timespinner_has_doublejump(world, player) and (state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player))),
|
||||
LocationData('Military Fortress (hangar)', 'Military soldiers bridge', 1337060),
|
||||
LocationData('Military Fortress (hangar)', 'Military giantess room', 1337061),
|
||||
LocationData('Military Fortress (hangar)', 'Military giantess bridge', 1337062),
|
||||
LocationData('Military Fortress (hangar)', 'Military B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('Military Fortress (hangar)', 'Military B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('Military Fortress (hangar)', 'Military pedestal', 1337065, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)),
|
||||
LocationData('The lab', 'Coffee break', 1337066),
|
||||
LocationData('The lab', 'Lower trash right', 1337067, lambda state: state._timespinner_has_doublejump(world, player)),
|
||||
LocationData('The lab', 'Lower trash left', 1337068, lambda state: state._timespinner_has_upwarddash(world, player)),
|
||||
@@ -95,7 +98,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData('Emperors tower', 'Dad\'s courtyard chest', 1337079, lambda state: state._timespinner_has_upwarddash(world, player)),
|
||||
LocationData('Emperors tower', 'Galactic sage room', 1337080),
|
||||
LocationData('Emperors tower', 'Bottom of Dad\'s right tower', 1337081),
|
||||
LocationData('Emperors tower', 'Wayyyy up there', 1337082),
|
||||
LocationData('Emperors tower', 'Wayyyy up there', 1337082, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
|
||||
LocationData('Emperors tower', 'Dad\'s left tower balcony', 1337083),
|
||||
LocationData('Emperors tower', 'Dad\'s Chambers chest', 1337084),
|
||||
LocationData('Emperors tower', 'Dad\'s Chambers pedestal', 1337085),
|
||||
@@ -180,19 +183,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData('Royal towers (upper)', 'Aelana\'s pedestal', 1337154),
|
||||
LocationData('Royal towers (upper)', 'Aelana\'s chest', 1337155),
|
||||
|
||||
# 1337176 - 1337176 Cantoran
|
||||
|
||||
# 1337177 - 1337236 Reserved
|
||||
|
||||
# 1337237 - 1337238 GyreArchives
|
||||
|
||||
# PyramidItemLocations
|
||||
LocationData('Ancient Pyramid (right)', 'Transition chest 1', 1337239),
|
||||
LocationData('Ancient Pyramid (right)', 'Transition chest 2', 1337240),
|
||||
LocationData('Ancient Pyramid (right)', 'Transition chest 3', 1337241),
|
||||
|
||||
# 1337242 - 1337245 GyreArchives
|
||||
|
||||
#AncientPyramidLocations
|
||||
LocationData('Ancient Pyramid (left)', 'Why not it\'s right there', 1337246),
|
||||
LocationData('Ancient Pyramid (left)', 'Conviction guarded room', 1337247),
|
||||
LocationData('Ancient Pyramid (right)', 'Pit secret room', 1337248, lambda state: state._timespinner_can_break_walls(world, player)),
|
||||
@@ -200,48 +191,75 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId)
|
||||
]
|
||||
|
||||
downloadable_locations: Tuple[LocationData, ...] = (
|
||||
# DownloadTerminals
|
||||
LocationData('Library', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Library', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)),
|
||||
# 1337158 Is Lost in time
|
||||
LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Library', 'V terminal 1', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library', 'V terminal 3', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Lab terminal right', 1337170, lambda state: state.has('Tablet', player))
|
||||
)
|
||||
# 1337156 - 1337170 Downloads
|
||||
if not world or is_option_enabled(world, player, "DownloadableItems"):
|
||||
location_table += (
|
||||
LocationData('Library', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Library', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
|
||||
# 1337158 Is Lost in time
|
||||
LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Library', 'V terminal 1', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library', 'V terminal 3', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||
LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)),
|
||||
LocationData('The lab (power off)', 'Lab terminal right', 1337170, lambda state: state.has('Tablet', player))
|
||||
)
|
||||
|
||||
gyre_archives_locations: Tuple[LocationData, ...] = (
|
||||
LocationData('The lab (upper)', 'Ravenlord post fight (pedestal)', 1337237, lambda state: state.has('Merchant Crow', player)),
|
||||
LocationData('Library top', 'Ifrit post fight (pedestal)', 1337238, lambda state: state.has('Kobo', player)),
|
||||
LocationData('The lab (upper)', 'Ravenlord pre fight', 1337242, lambda state: state.has('Merchant Crow', player)),
|
||||
LocationData('The lab (upper)', 'Ravenlord post fight (chest)', 1337243, lambda state: state.has('Merchant Crow', player)),
|
||||
LocationData('Library top', 'Ifrit pre fight', 1337244, lambda state: state.has('Kobo', player)),
|
||||
LocationData('Library top', 'Ifrit post fight (chest)', 1337245, lambda state: state.has('Kobo', player)),
|
||||
)
|
||||
# 1337176 - 1337176 Cantoran
|
||||
if not world or is_option_enabled(world, player, "Cantoran"):
|
||||
location_table += (
|
||||
LocationData('Left Side forest Caves', 'Cantoran', 1337176),
|
||||
)
|
||||
|
||||
cantoran_locations: Tuple[LocationData, ...] = (
|
||||
LocationData('Left Side forest Caves', 'Cantoran', 1337176),
|
||||
)
|
||||
# 1337177 - 1337198 Lore Checks
|
||||
if not world or is_option_enabled(world, player, "LoreChecks"):
|
||||
location_table += (
|
||||
LocationData('Lower lake desolation', 'Memory - Coyote Jump (Time Messenger)', 1337177),
|
||||
LocationData('Library', 'Memory - Waterway (A Message)', 1337178),
|
||||
LocationData('Library top', 'Memory - Library Gap (Lachiemi Sun)', 1337179),
|
||||
LocationData('Library top', 'Memory - Mr. Hat Portrait (Moonlit Night)', 1337180),
|
||||
LocationData('Varndagroth tower left', 'Memory - Left Elevator (Nomads)', 1337181, lambda state: state.has('Elevator Keycard', player)),
|
||||
LocationData('Varndagroth tower right (lower)', 'Memory - Siren Elevator (Childhood)', 1337182, lambda state: state._timespinner_has_keycard_B(world, player)),
|
||||
LocationData('Varndagroth tower right (lower)', 'Memory - Varndagroth Right Bottom (Faron)', 1337183),
|
||||
LocationData('Military Fortress', 'Memory - Bomber Climb (A Solution)', 1337184, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)),
|
||||
LocationData('The lab', 'Memory - Genza\'s Secret Stash 1 (An Old Friend)', 1337185, lambda state: state._timespinner_can_break_walls(world, player)),
|
||||
LocationData('The lab', 'Memory - Genza\'s Secret Stash 2 (Twilight Dinner)', 1337186, lambda state: state._timespinner_can_break_walls(world, player)),
|
||||
LocationData('Emperors tower', 'Memory - Way Up There (Final Circle)', 1337187, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
|
||||
LocationData('Forest', 'Journal - Forest Rats (Lachiem Expedition)', 1337188),
|
||||
LocationData('Forest', 'Journal - Forest Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)),
|
||||
LocationData('Castle Ramparts', 'Journal - Floating in Moat (Prime Edicts)', 1337190),
|
||||
LocationData('Castle Ramparts', 'Journal - Archer + Knight (Declaration of Independence)', 1337191),
|
||||
LocationData('Castle Keep', 'Journal - Under the Twins (Letter of Reference)', 1337192),
|
||||
LocationData('Castle Keep', 'Journal - Castle Loop Giantess (Political Advice)', 1337193),
|
||||
LocationData('Royal towers (lower)', 'Journal - Aleana\'s Room (Diplomatic Missive)', 1337194, lambda state: state._timespinner_has_pink(world, player)),
|
||||
LocationData('Royal towers (upper)', 'Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195),
|
||||
LocationData('Royal towers (upper)', 'Journal - Aleana Boss (Stained Letter)', 1337196),
|
||||
LocationData('Royal towers', 'Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
|
||||
LocationData('Caves of Banishment (Maw)', 'Journal - Lower Left Maw Caves (Naivety)', 1337198)
|
||||
)
|
||||
|
||||
if not world:
|
||||
return ( *location_table, *downloadable_locations, *gyre_archives_locations, *cantoran_locations )
|
||||
|
||||
if is_option_enabled(world, player, "DownloadableItems"):
|
||||
location_table.extend(downloadable_locations)
|
||||
if is_option_enabled(world, player, "GyreArchives"):
|
||||
location_table.extend(gyre_archives_locations)
|
||||
if is_option_enabled(world, player, "Cantoran"):
|
||||
location_table.extend(cantoran_locations)
|
||||
# 1337199 - 1337236 Reserved for future use
|
||||
|
||||
# 1337237 - 1337245 GyreArchives
|
||||
if not world or is_option_enabled(world, player, "GyreArchives"):
|
||||
location_table += (
|
||||
LocationData('Ravenlord\'s Lair', 'Ravenlord post fight (pedestal)', 1337237),
|
||||
LocationData('Ifrit\'s Lair', 'Ifrit post fight (pedestal)', 1337238),
|
||||
LocationData('Temporal Gyre', 'Gyre chest 1', 1337239),
|
||||
LocationData('Temporal Gyre', 'Gyre chest 2', 1337240),
|
||||
LocationData('Temporal Gyre', 'Gyre chest 3', 1337241),
|
||||
LocationData('Ravenlord\'s Lair', 'Ravenlord pre fight', 1337242),
|
||||
LocationData('Ravenlord\'s Lair', 'Ravenlord post fight (chest)', 1337243),
|
||||
LocationData('Ifrit\'s Lair', 'Ifrit pre fight', 1337244),
|
||||
LocationData('Ifrit\'s Lair', 'Ifrit post fight (chest)', 1337245),
|
||||
)
|
||||
|
||||
return tuple(location_table)
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ class Cantoran(Toggle):
|
||||
"Cantoran's fight and check are available upon revisiting his room"
|
||||
display_name = "Cantoran"
|
||||
|
||||
class LoreChecks(Toggle):
|
||||
"Memories and journal entries contain items."
|
||||
display_name = "Lore Checks"
|
||||
|
||||
class DamageRando(Toggle):
|
||||
"Each orb has a high chance of having lower base damage and a low chance of having much higher base damage."
|
||||
display_name = "Damage Rando"
|
||||
@@ -68,6 +72,7 @@ timespinner_options: Dict[str, Toggle] = {
|
||||
#"StinkyMaw": StinkyMaw,
|
||||
"GyreArchives": GyreArchives,
|
||||
"Cantoran": Cantoran,
|
||||
"LoreChecks": LoreChecks,
|
||||
"DamageRando": DamageRando,
|
||||
"DeathLink": DeathLink,
|
||||
}
|
||||
|
||||
@@ -14,15 +14,18 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Library'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Library top'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Military Fortress'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Military Fortress (hangar)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ravenlord\'s Lair'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Emperors tower'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'),
|
||||
@@ -40,6 +43,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Temporal Gyre'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
|
||||
@@ -68,6 +72,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
connect(world, player, names, 'Library', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_D(world, player))
|
||||
connect(world, player, names, 'Library', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Library top', 'Library')
|
||||
connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player))
|
||||
connect(world, player, names, 'Ifrit\'s Lair', 'Library top')
|
||||
connect(world, player, names, 'Varndagroth tower left', 'Library')
|
||||
connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (upper)', lambda state: state._timespinner_has_keycard_C(world, player))
|
||||
connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', lambda state: state._timespinner_has_keycard_B(world, player))
|
||||
@@ -86,14 +92,20 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player))
|
||||
connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Military Fortress', 'Varndagroth tower right (lower)', lambda state: state._timespinner_can_kill_all_3_bosses(world, player))
|
||||
connect(world, player, names, 'Military Fortress', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player))
|
||||
connect(world, player, names, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player))
|
||||
connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', lambda state: state._timespinner_has_doublejump(world, player))
|
||||
connect(world, player, names, 'Military Fortress (hangar)', 'Military Fortress')
|
||||
connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player))
|
||||
connect(world, player, names, 'Temporal Gyre', 'Military Fortress')
|
||||
connect(world, player, names, 'The lab', 'Military Fortress')
|
||||
connect(world, player, names, 'The lab', 'The lab (power off)', lambda state: state._timespinner_has_doublejump_of_npc(world, player))
|
||||
connect(world, player, names, 'The lab (power off)', 'The lab')
|
||||
connect(world, player, names, 'The lab (power off)', 'The lab (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
||||
connect(world, player, names, 'The lab (upper)', 'The lab (power off)')
|
||||
connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player))
|
||||
connect(world, player, names, 'The lab (upper)', 'Emperors tower', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
||||
connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (left)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player))
|
||||
connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)')
|
||||
connect(world, player, names, 'Emperors tower', 'The lab (upper)')
|
||||
connect(world, player, names, 'Skeleton Shaft', 'Lake desolation')
|
||||
connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player))
|
||||
|
||||
@@ -18,7 +18,7 @@ class TimespinnerWorld(World):
|
||||
game = "Timespinner"
|
||||
topology_present = True
|
||||
remote_items = False
|
||||
data_version = 4
|
||||
data_version = 6
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||
|
||||
Reference in New Issue
Block a user