Compare commits

...

72 Commits
0.2.2 ... 0.2.3

Author SHA1 Message Date
Fabian Dill
aff9114c35 0.2.3 2022-01-08 16:12:56 +01:00
Scipio Wright
f656f08f9b Docs: Cherry pick SM guide update from docs consolidation 2022-01-08 15:40:00 +01:00
Alchav
967e3028fd LTTP - Cap item prices at 4x
I think quadrupled prices will be plenty expensive, and this will stop people who pick "random" from getting 9999 priced items and potentially locking their multiworld behind absurd rupee grinds
2022-01-08 04:59:33 +01:00
Alchav
428af55bd9 LTTP shop price modifier tweak
Ensure shop prices are a multiple of 5 after price modifier
2022-01-07 18:11:31 +01:00
espeon65536
340725d395 OoT: add protection on starting inventory to be only giveable items 2022-01-07 16:01:28 +01:00
espeon65536
f8030393c8 OoT: If skip_child_zelda is on, set rule on Song from Impa to be giveable item 2022-01-07 16:01:28 +01:00
Fabian Dill
f6197d0a8d WebHost: add pretty print version of datapackage for human eyes 2022-01-07 03:32:51 +01:00
black-sliver
969ea5e6ee fix triggers for multiple slots from one yaml 2022-01-07 00:54:31 +01:00
Fabian Dill
d4c6268a46 Generate: allow meta to log-fail as opposed to exception-fail if category is missing in target 2022-01-06 22:01:18 +01:00
Fabian Dill
aeda76c058 WebHost: sort games by alphabet 2022-01-06 19:49:26 +01:00
Fabian Dill
9894d0672f Options: allow Choices to be hashed 2022-01-06 17:03:47 +01:00
Fabian Dill
d2e884b1d9 Options: allow Toggles to be hashed 2022-01-06 06:18:54 +01:00
Fabian Dill
80b3a5b1d4 WebHost: fix is_zipfile check for flask FileStorage objects
- and assorted cleanup
2022-01-06 06:09:15 +01:00
lordlou
a6a9989fcf SM small improvements (#190)
* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression
2022-01-05 20:15:19 +01:00
Jarno Westhof
0c3b5439e9 [Timespinner] Actually use the correct url in setup doc 2022-01-04 23:02:14 +01:00
Jarno Westhof
963e9d4bb5 [Timespinner] Updated timespinner setup docs (#184)
* [Timespinner] Updated setup docs
2022-01-04 22:56:53 +01:00
Fabian Dill
4dd7c63cab Generate: fix accessibility and progression_balancing override 2022-01-04 20:04:02 +01:00
espeon65536
03a892aded OoT updates (#160)
* OoT: disable mixed entrance pools and decoupled entrances for now

* OoT: fix error message crash in get_hint_area

* Oot Adjuster: kill zootdec if it's not the vanilla rom anymore

* OoT Adjuster: fix dmaTable issue
Adjuster should now work on compiled versions of the software

* OoT: don't skip dungeon items shuffled as any_dungeon for barren hints

* OoT: wrap zootdec remove in try-finally
2022-01-04 17:16:09 +01:00
Zach Parks
b3c1c0bbe8 RogueLegacy: Moved world definition from "legacy" to "rogue-legacy" to avoid confusion with deprecation terms 2022-01-04 04:27:51 +01:00
Chris Wilson
5a064b0979 [WebHost] weighted-settings: Ranges with a total distance <= 10 are always printed in full 2022-01-03 19:56:54 -05:00
Zach Parks
f06e565441 Add Rogue Legacy to Archipelago (#180) 2022-01-03 19:12:32 +01:00
Alchav
41fdafa3fb LTTP Shop updates (#177)
* Shop price modifier and non-lttp item price changes

* Item price modifier setting
2022-01-03 03:07:43 +01:00
Chris Wilson
27c528a6b3 [WebHost] weighted-settings: Add random, random-low, and random-high to range options 2022-01-02 19:57:26 -05:00
Chris Wilson
9623c1fffd [WebHost] weighted-settings: Add collapse/expand buttons to game divs 2022-01-02 18:55:38 -05:00
Chris Wilson
d4e0347d1d [WebHost] weighted-settings: Fix footer style and clean up yaml download 2022-01-02 18:45:45 -05:00
Chris Wilson
74bb057314 Implemented range settings 2022-01-02 18:31:15 -05:00
Jarno Westhof
b2980178d1 [Timespinner] Fixed logic of journal 2022-01-03 00:15:52 +01:00
Chris Wilson
08a0871168 Add game-jumping and hint text css to weighted-settings 2022-01-02 16:31:49 -05:00
Jarno Westhof
51fa00399d [Timespinner] Fixed logic for original wayyy up there location 2022-01-02 17:34:05 +01:00
Ross Bemrose
7622f7f28f Timespinner: Fix missing double-jump checks for LoreChecks locations (#181) 2022-01-02 16:33:29 +01:00
Chris Wilson
d98d693369 Remove debug logging 2022-01-01 17:05:08 -05:00
Chris Wilson
c7e8692964 Fix merge conflict. Very minor difference. 2022-01-01 17:02:51 -05:00
Chris Wilson
0431c3fce0 Much more work on weighted-setting page. Still needs support for range options and item/location settings. 2022-01-01 16:59:58 -05:00
Colin Lenzen
411f0e40b6 Timespinner - Add Lore Checks checks (#171) 2022-01-01 20:44:45 +01:00
Jarno Westhof
a5d2046a87 [Docs] More Links (#179)
* [Docs] More Links

* [Docs] Moved link for data package object
2022-01-01 20:29:38 +01:00
Fabian Dill
f8893a7ed3 WebHost: check uploads against zip magic number instead of .zip 2022-01-01 17:18:48 +01:00
Fabian Dill
93ac018400 SNIClient: make SNI finder a bit smarter 2022-01-01 15:46:08 +01:00
Fabian Dill
6b852d6e1a WebHost Options: hidden games should remain functional, just hidden. 2022-01-01 03:12:32 +01:00
Chris Wilson
06dc76a78b Added locations to generated weighted-settings.json. In-progress /weighted-settings page available on WebHost, currently non-functional as I work on JS backend stuff 2021-12-31 14:42:04 -05:00
Jarno Westhof
4db4b5305e [Docs] Added links to client implementations (#167) 2021-12-31 20:05:36 +01:00
Chris Wilson
c550fdaee8 WebHost now generates a weighted-settings.json file for use with the upcoming weighted-settings page. 2021-12-31 13:22:23 -05:00
Fabian Dill
d13b7988b7 Core: undo change that made Python 3.9 required 2021-12-31 15:08:30 +01:00
Brad Humphrey
d437f0105a Test remaining locations after swapping 2021-12-30 19:06:03 +01:00
Alchav
b65618030f Remove unnecessary logging.info 2021-12-30 16:55:33 +01:00
Alchav
01a2376b74 Let make_dungeon set up items, then replace 2021-12-30 16:55:33 +01:00
Alchav
d10ddb17b6 Let make_dungeon set up items, then replace 2021-12-30 16:55:33 +01:00
Alchav
c42d489bf7 Pull dungeon item replacements from diff extras 2021-12-30 16:55:33 +01:00
Alchav
8fef6b8d8c Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
35b1178c20 Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
c0f95755ff Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
b7676a3da2 Add "Start With" option for dungeon items 2021-12-30 16:55:33 +01:00
Brad Humphrey
3d65719170 Remove dependency on pytest 2021-12-30 16:55:08 +01:00
Brad Humphrey
18d262c1ae Add test for minimal accessibility 2021-12-30 16:55:08 +01:00
Brad Humphrey
e5fedb90a6 Process swaped items last 2021-12-30 16:55:08 +01:00
Brad Humphrey
dc82b384c5 Add comment about swap count 2021-12-30 16:55:08 +01:00
Brad Humphrey
2f56e40fb7 Include player information in swapped item count 2021-12-30 16:55:08 +01:00
Brad Humphrey
d719eb356f Don't allow items to swap infinitly 2021-12-30 16:55:08 +01:00
Brad Humphrey
6a34fe5184 Add fallback item swap for unreachable items 2021-12-30 16:55:08 +01:00
Brad Humphrey
461961c3be Add test locations to region 2021-12-30 16:55:08 +01:00
Brad Humphrey
39869bcdc5 Add basic fill test cases 2021-12-30 16:55:08 +01:00
Jarno Westhof
a10d7ae5b9 [Timespinner] Fixed some placement logics regarding gyre archives & military fortress
Renamed 'Transition chest #' to 'Gyre chest #'
2021-12-30 16:50:04 +01:00
Fabian Dill
4ed45248eb LttP: Rename "Dark World Shop" overworld door to Village of Outcasts Shop. Note: Now the overworld door, Region, Shop and inside door are named the same. 2021-12-29 11:08:23 +01:00
Fabian Dill
6e4b255be5 Options: make common options overridable in a game section
WebHost: add prog balancing and accessibility to settings page
2021-12-28 18:43:52 +01:00
Hussein Farran
2e56c226db WebHost: Patch downloads now prompt you with a dialog box/file save dialog. 2021-12-28 14:18:49 +01:00
Hussein Farran
844ff402cd WebHost: Improve player enumeration performance in upload.py 2021-12-28 14:18:49 +01:00
Hussein Farran
ec570be178 WebHost: Improve performance in player slot tracking during upload. 2021-12-28 14:18:49 +01:00
Hussein Farran
3508cf21c7 WebHost: Add game listing for all players on room info page. 2021-12-28 14:18:49 +01:00
alwaysintreble
1f4ddc295a tutorials: Point lttp tutorial to SNC instead of Z3. Update some deprecated text. 2021-12-27 22:34:57 +01:00
Jarno Westhof
4ef0e054d6 [TS] Move 3 transition chest under gyre archives flag + some refactoring 2021-12-27 15:39:42 +01:00
Yussur Mustafa Oraji
61310c50d7 Use absolute path when starting SNI
Causes reliability issues when relative path is used.
2021-12-27 15:39:14 +01:00
wafflesoup
6eab838a70 Update plando_en.md
fixed capitalization in Timespinner example
2021-12-25 22:44:07 +01:00
Fabian Dill
52e01c0925 Factorio: fill in some missing doc strings 2021-12-22 14:00:41 +01:00
66 changed files with 2507 additions and 307 deletions

5
.gitignore vendored
View File

@@ -155,4 +155,7 @@ Archipelago.zip
#minecraft server stuff
jdk*/
minecraft*/
minecraft*/
#pyenv
.python-version

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

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

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

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

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

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

View 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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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