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

3
.gitignore vendored
View File

@@ -156,3 +156,6 @@ Archipelago.zip
#minecraft server stuff #minecraft server stuff
jdk*/ jdk*/
minecraft*/ minecraft*/
#pyenv
.python-version

View File

@@ -782,10 +782,9 @@ class RegionType(int, Enum):
class Region(object): class Region(object):
def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None):
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
self.name = name self.name = name
self.type = type self.type = type_
self.entrances = [] self.entrances = []
self.exits = [] self.exits = []
self.locations = [] self.locations = []
@@ -1210,8 +1209,6 @@ class Spoiler():
if self.world.players > 1: if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
outfile.write('Game: %s\n' % self.world.game[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(): for f_option, option in Options.per_game_common_options.items():
write_option(f_option, option) write_option(f_option, option)
options = self.world.worlds[player].options options = self.world.worlds[player].options

80
Fill.py
View File

@@ -2,8 +2,10 @@ import logging
import typing import typing
import collections import collections
import itertools 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.generic import PlandoItem
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
@@ -12,30 +14,35 @@ class FillError(RuntimeError):
pass pass
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False, def sweep_from_pool(base_state: CollectionState, itempool):
lock=False): new_state = base_state.copy()
def sweep_from_pool(): for item in itempool:
new_state = base_state.copy() new_state.collect(item, True)
for item in itempool: new_state.sweep_for_events()
new_state.collect(item, True) return new_state
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 = [] unplaced_items = []
placements = [] placements = []
reachable_items = {} swapped_items = Counter()
reachable_items: dict[str, deque] = {}
for item in itempool: 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: 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: for item in items_to_place:
itempool.remove(item) 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) has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place: for item_to_place in items_to_place:
spot_to_fill: Location = None
if world.accessibility[item_to_place.player] == 'minimal': if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) if single_player_placement else not has_beaten_game 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): for i, location in enumerate(locations):
if (not single_player_placement or location.player == item_to_place.player) \ 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): 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 # skipping a scan for the element
break break
else: else:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots.
unplaced_items.append(item_to_place) # try swaping this item with previously placed items
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game(): for(i, location) in enumerate(placements):
logging.warning( placed_item = location.item
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})') # Unplaceable items can sometimes be swapped infinitely. Limit the
continue # number of times we will swap an individual item to prevent this
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. ' if swapped_items[placed_item.player, placed_item.name] > 0:
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') 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) world.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock spot_to_fill.locked = lock

View File

@@ -23,6 +23,7 @@ import Options
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
import copy
categories = set(AutoWorldRegister.world_types) categories = set(AutoWorldRegister.world_types)
@@ -148,7 +149,7 @@ def main(args=None, callback=ERmain):
if category_name is None: if category_name is None:
weights_cache[path][key] = option weights_cache[path][key] = option
elif category_name not in weights_cache[path]: 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: else:
weights_cache[path][category_name][key] = option 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: 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"]: for option_set in weights["linked_options"]:
if "name" not in option_set: if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.") 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: 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__ weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers): for i, option_set in enumerate(triggers):
try: try:
@@ -469,7 +470,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret = argparse.Namespace() ret = argparse.Namespace()
for option_key in Options.per_game_common_options: 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.") raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights) 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(): for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option) handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items(): 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: if "items" in plando_options:
ret.plando_items = roll_item_plando(world_type, game_weights) ret.plando_items = roll_item_plando(world_type, game_weights)
if ret.game == "Minecraft" or ret.game == "Ocarina of Time": if ret.game == "Minecraft" or ret.game == "Ocarina of Time":

View File

@@ -235,11 +235,11 @@ class Context:
with open(multidatapath, 'rb') as f: with open(multidatapath, 'rb') as f:
data = f.read() 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 self.data_filename = multidatapath
@staticmethod @staticmethod
def _decompress(data: bytes) -> dict: def decompress(data: bytes) -> dict:
format_version = data[0] format_version = data[0]
if format_version != 1: if format_version != 1:
raise Exception("Incompatible multidata.") 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.Options import cosmetic_options, sfx_options
from worlds.oot.Rom import Rom, compress_rom_file from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
from Utils import local_path from Utils import local_path
logger = logging.getLogger('OoTAdjuster') logger = logging.getLogger('OoTAdjuster')
@@ -211,9 +212,11 @@ def adjust(args):
ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless' ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless'
ootworld.death_link = args.deathlink ootworld.death_link = args.deathlink
delete_zootdec = False
if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']: if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']:
# Load up the ROM # Load up the ROM
rom = Rom(file=args.rom, force_use=True) rom = Rom(file=args.rom, force_use=True)
delete_zootdec = True
elif os.path.splitext(args.rom)[-1] == '.apz5': elif os.path.splitext(args.rom)[-1] == '.apz5':
# Load vanilla ROM # Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True) rom = Rom(file=args.vanilla_rom, force_use=True)
@@ -222,15 +225,21 @@ def adjust(args):
else: else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5") raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
# Call patch_cosmetics # Call patch_cosmetics
patch_cosmetics(ootworld, rom) try:
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink) patch_cosmetics(ootworld, rom)
# Output new file rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
path_pieces = os.path.splitext(args.rom) # Output new file
decomp_path = path_pieces[0] + '-adjusted-decomp.n64' path_pieces = os.path.splitext(args.rom)
comp_path = path_pieces[0] + '-adjusted.n64' decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
rom.write_to_file(decomp_path) comp_path = path_pieces[0] + '-adjusted.n64'
compress_rom_file(decomp_path, comp_path) rom.write_to_file(decomp_path)
os.remove(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 return comp_path
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -125,6 +125,8 @@ class Toggle(Option):
def get_option_name(cls, value): def get_option_name(cls, value):
return ["No", "Yes"][int(value)] return ["No", "Yes"][int(value)]
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class DefaultOnToggle(Toggle): class DefaultOnToggle(Toggle):
default = 1 default = 1
@@ -184,6 +186,8 @@ class Choice(Option):
else: else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") 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): class Range(Option, int):
range_start = 0 range_start = 0
@@ -334,7 +338,7 @@ class Accessibility(Choice):
Locations: ensure everything can be reached and acquired. Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired. Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired.""" Minimal: ensure what is needed to reach your goal can be acquired."""
displayname = "Accessibility"
option_locations = 0 option_locations = 0
option_items = 1 option_items = 1
option_minimal = 2 option_minimal = 2
@@ -344,6 +348,7 @@ class Accessibility(Choice):
class ProgressionBalancing(DefaultOnToggle): class ProgressionBalancing(DefaultOnToggle):
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early.""" """A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
displayname = "Progression Balancing"
common_options = { common_options = {
@@ -395,6 +400,7 @@ class DeathLink(Toggle):
per_game_common_options = { per_game_common_options = {
**common_options, # can be overwritten per-game
"local_items": LocalItems, "local_items": LocalItems,
"non_local_items": NonLocalItems, "non_local_items": NonLocalItems,
"start_inventory": StartInventory, "start_inventory": StartInventory,

View File

@@ -14,6 +14,7 @@ Currently, the following games are supported:
* Super Metroid * Super Metroid
* Secret of Evermore * Secret of Evermore
* Final Fantasy * Final Fantasy
* Rogue Legacy
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). 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 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): if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path) sni_path = Utils.local_path(sni_path)
if os.path.isdir(sni_path): if os.path.isdir(sni_path):
for file in os.listdir(sni_path): dir_entry: os.DirEntry
lower_file = file.lower() for dir_entry in os.scandir(sni_path):
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni": if dir_entry.is_file():
sni_path = os.path.join(sni_path, 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): if os.path.isfile(sni_path):
snes_logger.info(f"Attempting to start {sni_path}") snes_logger.info(f"Attempting to start {sni_path}")
import sys import sys
if not sys.stdout: # if it spawns a visible console, may as well populate it 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: 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) stderr=subprocess.DEVNULL)
else: else:
snes_logger.info( snes_logger.info(

View File

@@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
build: int build: int
__version__ = "0.2.2" __version__ = "0.2.3"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
from yaml import load, dump, safe_load from yaml import load, dump, safe_load

View File

@@ -89,6 +89,11 @@ def start_playing():
return render_template(f"startPlaying.html") return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages # Player settings pages
@app.route('/games/<string:game>/player-settings') @app.route('/games/<string:game>/player-settings')
def player_settings(game): def player_settings(game):
@@ -188,6 +193,15 @@ def discord():
return redirect("https://discord.gg/archipelago") 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 WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it 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 from worlds import network_data_package
return network_data_package return network_data_package
@api_endpoints.route('/datapackage_version') @api_endpoints.route('/datapackage_version')
@cache.cached() @cache.cached()
def get_datapackge_versions(): def get_datapackge_versions():

View File

@@ -76,7 +76,7 @@ class WebHostContext(Context):
else: else:
self.port = get_random_port() 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 @db_session
def init_save(self, enabled: bool = True): def init_save(self, enabled: bool = True):

View File

@@ -11,6 +11,8 @@ target_folder = os.path.join("WebHostLib", "static", "generated")
def create(): def create():
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
def dictify_range(option): def dictify_range(option):
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0, data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
option.default: 50} option.default: 50}
@@ -25,15 +27,24 @@ def create():
return list(default_value) return list(default_value)
return 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(): 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( 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, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter, 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: with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res) f.write(res)
@@ -47,7 +58,7 @@ def create():
} }
game_options = {} game_options = {}
for option_name, option in world.options.items(): for option_name, option in all_options.items():
if option.options: if option.options:
game_options[option_name] = this_option = { game_options[option_name] = this_option = {
"type": "select", "type": "select",
@@ -86,4 +97,14 @@ def create():
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True) 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: 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: - item:
Empire Orb: 1 Empire Orb: 1
Radiant Orb: 1 Radiant Orb: 1
location: Starter Chest 1 location: Starter chest 1
from_pool: true from_pool: true
world: true world: true
percentage: 50 percentage: 50

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 # Super Metroid Setup Guide
## Required Software ## Required Software
- [Super Metroid Client](https://github.com/ArchipelagoMW/SuperMetroidClient/releases)
- **sniConnector.lua** (located on the client download page) - SNI Client
- [SNI](https://github.com/alttpo/sni/releases) (Included in the Super Metroid Client) - Included in Archipelago download
- Hardware or software capable of loading and playing SNES ROM files - Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI - An emulator capable of connecting to SNI such as:
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), - snes9x Multitroid
[BizHawk](http://tasvideos.org/BizHawk.html)) from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
- Your Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc` - 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 ## Installation Procedures
### Windows Setup ### 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. 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 3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
for launching ROM files. files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember. 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...** 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** 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** 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 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
the folder you extracted in step one. extracted in step one.
### Macintosh Setup ### Macintosh Setup
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help. - We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
## Create a Config (.yaml) File ## Create a Config (.yaml) File
### What is a config file and why do I need one? ### 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 See the guide on setting up a basic YAML at the Archipelago setup
each player to enjoy an experience customized for their taste, and different players in the same multiworld guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
can all have different options.
### Where do I get a config file? ### 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 ### 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 ## 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. 2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link. 3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file. 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 5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from the
the patch file, and open your emulator for you. 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. 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 ## Joining a MultiWorld Game
### Obtain your patch file and create your ROM ### 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 When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
launch the client, and will also create your ROM in the same place as your patch file. 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 ### Connect to the client
#### With an emulator #### 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 When the client launched automatically, SNI should have also automatically launched in the background. If this is its
Firewall. first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x Multitroid ##### snes9x Multitroid
1. Load your ROM file if it hasn't already been loaded. 1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting** 2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...** 3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...** 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 ##### 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` `Config --> Cores --> SNES --> BSNES`
Once you have changed the loaded core, you must restart BizHawk. Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded. 2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console** 3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script. 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 #### 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 This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
[on this page](http://usb2snes.com/#supported-platforms). 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. 1. Close your emulator, which may have auto-launched.
2. Power on your device and load the ROM. 2. Power on your device and load the ROM.
### Connect to the Archipelago Server ### 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 The patch file which launched your client should have automatically connected you to the AP Server. There are a few
Status: Connected". 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 ### 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 ## 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. 1. Collect config files from your players.
2. Create a zip file containing your players' config files. 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. 4. Wait a moment while the seed is generated.
5. When the seed is generated, you will be redirected to a "Seed Info" page. 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, 6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
so they may download their patch files from there. 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 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. 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. 8. Once all players have joined, you may begin playing.

View File

@@ -2,7 +2,7 @@
## Required Software ## 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) - [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
## General Concept ## General Concept
@@ -11,7 +11,7 @@ The timespinner Randomizer loads Timespinner.exe from the same folder, and alter
## Installation Procedures ## 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 ## Joining a MultiWorld Game
@@ -23,38 +23,8 @@ Download latest version of [Timespinner Randomizer](https://github.com/JarnoWest
5. Select "Connect" 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 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 ## Where do I get a config file?
An example YAML would look like this: 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.
```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
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds * 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 * 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 # A Link to the Past Randomizer Setup Guide
## Required Software ## Required Software
- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with - [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases) included with the main Archipelago install
[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) or [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/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 - 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 Z3Client and SNIClient) - [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 - Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI - An emulator capable of connecting to SNI
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
@@ -75,8 +75,9 @@ Firewall.
3. Click on **New Lua Script Window...** 3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...** 4. In the new window, click **Browse...**
5. Select the connector lua file included with your client 5. Select the connector lua file included with your client
- Z3Client users should download `sniConnector.lua` from the client download page - SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/sni/lua` - 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 ##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following 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** 3. Click on the Tools menu and click on **Lua Console**
4. Click Script -> Open Script... 4. Click Script -> Open Script...
5. Select the `Connector.lua` file you downloaded above 5. Select the `Connector.lua` file you downloaded above
- Z3Client users should download `sniConnector.lua` from the client download page - SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/sni/lua` - SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if
6. Run the script by double-clicking it in the listing the emulator is 64-bit or 32-bit.
#### With hardware #### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not 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 ### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations 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 ## 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](/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; border-radius: 3px;
width: 500px; 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 %} {%- endmacro %}
{% macro list_patches_room(room) %} {% macro list_patches_room(room) %}
{% if room.seed.slots %} {% 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") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
{% if patch.game == "Minecraft" %} <tr>
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}"> <td>{{ patch.player_id }}</td>
APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> <td>{{ patch.player_name }}</td>
{% elif patch.game == "Factorio" %} <td>{{ patch.game }}</td>
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}"> <td>
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> {% if patch.game == "Minecraft" %}
{% elif patch.game == "Ocarina of Time" %} <a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}"> Download APMC File...</a>
APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> {% elif patch.game == "Factorio" %}
{% else %} <a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}"> Download Factorio Mod...</a>
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> {% elif patch.game == "Ocarina of Time" %}
{% endif %} <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 %} {% endfor %}
</ul> </tbody>
</table>
{% endif %} {% endif %}
{%- endmacro -%} {%- endmacro -%}

View File

@@ -29,13 +29,6 @@ game:
requires: requires:
version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected. version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games: # 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) %} {%- macro range_option(option) %}
# you can add additional values between minimum and maximum # you can add additional values between minimum and maximum

View File

@@ -9,7 +9,7 @@
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="games"> <div id="games">
<h1>Currently Supported Games</h1> <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> <h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
<p> <p>
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a> <a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>

View File

@@ -2,7 +2,7 @@
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>Generate Game</title> <title>User Content</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/userContent.css") }}" /> <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> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/userContent.js") }}"></script>
{% endblock %} {% 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) result = _multidata_cache.get(room.seed.id, None)
if result: if result:
return 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 # 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'] locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations']
names: Dict[int, Dict[int, str]] = multidata["names"] 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"): elif file.filename.endswith(".archipelago"):
try: try:
multidata = zfile.open(file).read() multidata = zfile.open(file).read()
MultiServer.Context._decompress(multidata)
except: except:
flash("Could not load multidata. File may be corrupted or incompatible.") flash("Could not load multidata. File may be corrupted or incompatible.")
multidata = None multidata = None
if multidata: 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 flush() # commit slots
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
id=sid if sid else uuid.uuid4()) id=sid if sid else uuid.uuid4())
@@ -92,7 +100,7 @@ def uploads():
if file.filename == '': if file.filename == '':
flash('No selected file') flash('No selected file')
elif file and allowed_file(file.filename): elif file and allowed_file(file.filename):
if file.filename.endswith(".zip"): if zipfile.is_zipfile(file):
with zipfile.ZipFile(file, 'r') as zfile: with zipfile.ZipFile(file, 'r') as zfile:
res = upload_zip_to_db(zfile) res = upload_zip_to_db(zfile)
if type(res) == str: if type(res) == str:
@@ -100,12 +108,12 @@ def uploads():
elif res: elif res:
return redirect(url_for("view_seed", seed=res.id)) return redirect(url_for("view_seed", seed=res.id))
else: else:
# noinspection PyBroadException
try: try:
multidata = file.read() multidata = file.read()
MultiServer.Context._decompress(multidata) MultiServer.Context.decompress(multidata)
except: except:
flash("Could not load multidata. File may be corrupted or incompatible.") flash("Could not load multidata. File may be corrupted or incompatible.")
raise
else: else:
seed = Seed(multidata=multidata, owner=session["_id"]) seed = Seed(multidata=multidata, owner=session["_id"])
flush() # place into DB and generate ids 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. 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 ## 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. 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 #### Arguments
| Name | Type | Notes | | 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` | | 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.| | 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. | | 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. || | 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. | | 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_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. | | 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. | | 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. | | 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. | | 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. | | 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. | | 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 | | Name | Type | Notes |
| ---- | ---- | ----- | | ---- | ---- | ----- |
| index | int | The next empty slot in the list of items for the receiving client. | | 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 ### LocationInfo
Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted. Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted.
#### Arguments #### Arguments
| Name | Type | Notes | | 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 ### RoomUpdate
Sent when there is a need to update information about the present game session. Generally useful for async games. 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 | | Name | Type | Notes |
| ---- | ---- | ----- | | ---- | ---- | ----- |
| hint_points | int | New argument. The client's current hint points. | | hint_points | int | New argument. The client's current hint points. |
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. | | players | list\[[NetworkPlayer](#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. | | 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 | Should never be sent as an update, if needed is the inverse of checked_locations. | | 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. 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 #### Arguments
| Name | Type | Notes | | 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. | | 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. | | 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. | | found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
### DataPackage ### DataPackage
@@ -169,7 +173,7 @@ Sent to clients to provide what is known as a 'data package' which contains info
#### Arguments #### Arguments
| Name | Type | Notes | | 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 ### Bounced
Sent to clients after a client requested this message be sent to them, more info in the Bounce package. 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` | | 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. | | name | str | The player name for this client. |
| uuid | str | Unique identifier for player 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) | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
#### Authentication #### Authentication

View File

@@ -64,7 +64,7 @@ generator:
# general weights file, within the stated player_files_path location # 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 # gets used if players is higher than the amount of per-player files found to fill remaining slots
weights_file_path: "weights.yaml" 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" meta_file_path: "meta.yaml"
# Create a spoiler file # Create a spoiler file
# 0 -> None # 0 -> None

View File

@@ -9,6 +9,7 @@ Utils.local_path.cached_path = file_path
from BaseClasses import MultiWorld, CollectionState from BaseClasses import MultiWorld, CollectionState
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
world: MultiWorld world: MultiWorld
_state_cache = {} _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.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_entrances from worlds.alttp.EntranceShuffle import link_entrances
from worlds.alttp.InvertedRegions import mark_dark_world_regions 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.Items import ItemFactory
from worlds.alttp.Regions import create_regions from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from worlds import AutoWorld from worlds import AutoWorld
class TestMinor(TestBase): class TestMinor(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
@@ -30,7 +30,9 @@ class TestMinor(TestBase):
self.world.worlds[1].create_items() self.world.worlds[1].create_items()
self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.required_medallions[1] = ['Ether', 'Quake']
self.world.itempool.extend(get_dungeon_item_pool(self.world)) 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 1', 1).item = None
self.world.get_location('Agahnim 2', 1).item = None self.world.get_location('Agahnim 2', 1).item = None
mark_dark_world_regions(self.world, 1) mark_dark_world_regions(self.world, 1)

View File

@@ -2549,7 +2549,7 @@ DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
'Big Bomb Shop', 'Big Bomb Shop',
'Dark Death Mountain Fairy', 'Dark Death Mountain Fairy',
'Dark Lake Hylia Shop', 'Dark Lake Hylia Shop',
'Dark World Shop', 'Village of Outcasts Shop',
'Red Shield Shop', 'Red Shield Shop',
'Mire Shed', 'Mire Shed',
'East Dark World Hint', 'East Dark World Hint',
@@ -2626,7 +2626,7 @@ Bomb_Shop_Single_Cave_Doors = ['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',
'Dark World Lumberjack Shop', 'Dark World Lumberjack Shop',
'Dark World Potion Shop', 'Dark World Potion Shop',
'Archery Game', 'Archery Game',
@@ -2837,7 +2837,7 @@ Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
'C-Shaped House', 'C-Shaped House',
'Bumper Cave (Top)', 'Bumper Cave (Top)',
'Dark Lake Hylia Shop', 'Dark Lake Hylia Shop',
'Dark World Shop', 'Village of Outcasts Shop',
'Red Shield Shop', 'Red Shield Shop',
'Mire Shed', 'Mire Shed',
'East Dark World Hint', 'East Dark World Hint',
@@ -2883,7 +2883,7 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
'Red Shield Shop', 'Red Shield Shop',
'Inverted Dark Sanctuary', 'Inverted Dark Sanctuary',
'Fortune Teller (Dark)', 'Fortune Teller (Dark)',
'Dark World 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',
@@ -3543,7 +3543,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'),
('Red Shield Shop', 'Red Shield Shop'), ('Red Shield Shop', 'Red Shield Shop'),
('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'),
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('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 Lumberjack Shop', 'Dark World Lumberjack Shop'),
('Dark World Potion Shop', 'Dark World Potion Shop'), ('Dark World Potion Shop', 'Dark World Potion Shop'),
('Archery Game', 'Archery Game'), ('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'), ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'),
('Red Shield Shop', 'Red Shield Shop'), ('Red Shield Shop', 'Red Shield Shop'),
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('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 Lumberjack Shop', 'Dark World Lumberjack Shop'),
('Dark World Potion Shop', 'Dark World Potion Shop'), ('Dark World Potion Shop', 'Dark World Potion Shop'),
('Archery Game', 'Archery Game'), ('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)), '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)), '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)), '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 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)), '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)), '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', 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', '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']), '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, '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_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'), 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 worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError from Fill import FillError
from worlds.alttp.Items import ItemFactory, GetBeemizerItem 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. # 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. # 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): def generate_itempool(world):
player = world.player player = world.player
world = world.world world = world.world
if world.difficulty[player] not in difficulties: if world.difficulty[player] not in difficulties:
raise NotImplementedError(f"Diffulty {world.difficulty[player]}") raise NotImplementedError(f"Diffulty {world.difficulty[player]}")
if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', 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) 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] 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': if world.goal[player] == 'icerodhunt':
for item in dungeon_items: for item in dungeon_items:
world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player))
world.push_precollected(item) world.push_precollected(item)
else: 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]) world.itempool.extend([item for item in dungeon_items])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # 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) # 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) # 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) place_item(key_location, item_to_place)
else: else:
pool.extend([item_to_place]) pool.extend([item_to_place])
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
additional_pieces_to_place) additional_pieces_to_place)

View File

@@ -34,6 +34,7 @@ class DungeonItem(Choice):
option_own_world = 2 option_own_world = 2
option_any_world = 3 option_any_world = 3
option_different_world = 4 option_different_world = 4
option_start_with = 6
alias_true = 3 alias_true = 3
alias_false = 0 alias_false = 0
@@ -90,6 +91,11 @@ class ShopItemSlots(Range):
range_start = 0 range_start = 0
range_end = 30 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): class WorldState(Choice):
option_standard = 1 option_standard = 1
@@ -305,6 +311,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"killable_thieves": KillableThieves, "killable_thieves": KillableThieves,
"bush_shuffle": BushShuffle, "bush_shuffle": BushShuffle,
"shop_item_slots": ShopItemSlots, "shop_item_slots": ShopItemSlots,
"shop_price_modifier": ShopPriceModifier,
"tile_shuffle": TileShuffle, "tile_shuffle": TileShuffle,
"ow_palettes": OWPalette, "ow_palettes": OWPalette,
"uw_palettes": UWPalette, "uw_palettes": UWPalette,

View File

@@ -176,7 +176,7 @@ def create_regions(world, player):
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), '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', 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']), '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, '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_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'), 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', 'C-Shaped House': 'The NE house in Village of Outcasts',
'Dark Death Mountain Fairy': 'The SW cave on dark DM', 'Dark Death Mountain Fairy': 'The SW cave on dark DM',
'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', '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', 'Red Shield Shop': 'The fenced in building',
'Mire Shed': 'The western hut in the mire', 'Mire Shed': 'The western hut in the mire',
'East Dark World Hint': 'The dark cave near the eastmost portal', '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', 'Red Shield Shop',
'Dark Sanctuary Hint', 'Dark Sanctuary Hint',
'Fortune Teller (Dark)', 'Fortune Teller (Dark)',
'Dark World Shop', 'Village of Outcasts Shop',
'Dark World Lumberjack Shop', 'Dark World Lumberjack Shop',
'Thieves Town', 'Thieves Town',
'Skull Woods First Section Door', '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: 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. # 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)))) 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 # 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)))) 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)': elif bombshop_entrance.name == 'Bumper Cave (Bottom)':

View File

@@ -247,7 +247,12 @@ def ShopSlotFill(world):
item_name = location.item.name item_name = location.item.name
if location.item.game != "A Link to the Past": 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 elif any(x in item_name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
price = world.random.randrange(1, 7) price = world.random.randrange(1, 7)
@@ -258,7 +263,8 @@ def ShopSlotFill(world):
else: else:
price = world.random.randrange(8, 56) 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) location.item.player if location.item.player != location.player else 0)
if 'P' in world.shop_shuffle[location.player]: if 'P' in world.shop_shuffle[location.player]:
price_to_funny_price(shop.inventory[location.shop_slot], world, 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): 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" displayname = "Progressive Technologies"
option_off = 0 option_off = 0
option_grouped_random = 1 option_grouped_random = 1
@@ -151,17 +153,19 @@ class RecipeIngredients(Choice):
class FactorioStartItems(ItemDict): class FactorioStartItems(ItemDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
displayname = "Starting Items" displayname = "Starting Items"
verify_item_name = False verify_item_name = False
default = {"burner-mining-drill": 19, "stone-furnace": 19} default = {"burner-mining-drill": 19, "stone-furnace": 19}
class FactorioFreeSampleBlacklist(OptionSet): class FactorioFreeSampleBlacklist(OptionSet):
"""Set of items that should never be granted from Free Samples"""
displayname = "Free Sample Blacklist" displayname = "Free Sample Blacklist"
class FactorioFreeSampleWhitelist(OptionSet): 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" displayname = "Free Sample Whitelist"
@@ -180,6 +184,7 @@ class EvolutionTrapCount(TrapCount):
class EvolutionTrapIncrease(Range): class EvolutionTrapIncrease(Range):
"""How much an Evolution Trap increases the enemy evolution"""
displayname = "Evolution Trap % Effect" displayname = "Evolution Trap % Effect"
range_start = 1 range_start = 1
default = 10 default = 10
@@ -187,6 +192,8 @@ class EvolutionTrapIncrease(Range):
class FactorioWorldGen(OptionDict): 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" displayname = "World Generation"
# FIXME: do we want default be a rando-optimized default or in-game DS? # FIXME: do we want default be a rando-optimized default or in-game DS?
value: typing.Dict[str, typing.Dict[str, typing.Any]] value: typing.Dict[str, typing.Dict[str, typing.Any]]
@@ -320,6 +327,7 @@ class FactorioWorldGen(OptionDict):
class ImportedBlueprint(DefaultOnToggle): class ImportedBlueprint(DefaultOnToggle):
"""Allow or Disallow Blueprints from outside the current savegame."""
displayname = "Blueprints" displayname = "Blueprints"

View File

@@ -68,16 +68,12 @@ class HKWorld(World):
self.world.itempool += pool self.world.itempool += pool
def set_rules(self): def set_rules(self):
set_rules(self.world, self.player) set_rules(self.world, self.player)
def create_regions(self): def create_regions(self):
create_regions(self.world, self.player) create_regions(self.world, self.player)
def generate_output(self):
pass # Hollow Knight needs no output files
def fill_slot_data(self): def fill_slot_data(self):
slot_data = {} slot_data = {}
for option_name in self.options: 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))) 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: else:
return spot.name return spot.name

View File

@@ -191,8 +191,8 @@ world_options: typing.Dict[str, type(Option)] = {
"owl_drops": OwlDrops, "owl_drops": OwlDrops,
"warp_songs": WarpSongs, "warp_songs": WarpSongs,
"spawn_positions": SpawnPositions, "spawn_positions": SpawnPositions,
"mix_entrance_pools": MixEntrancePools, # "mix_entrance_pools": MixEntrancePools,
"decouple_entrances": DecoupleEntrances, # "decouple_entrances": DecoupleEntrances,
"triforce_hunt": TriforceHunt, "triforce_hunt": TriforceHunt,
"triforce_goal": TriforceGoal, "triforce_goal": TriforceGoal,
"extra_triforce_percentage": ExtraTriforces, "extra_triforce_percentage": ExtraTriforces,

View File

@@ -282,7 +282,7 @@ class Rom(BigStream):
def compress_rom_file(input_file, output_file): def compress_rom_file(input_file, output_file):
compressor_path = data_path("Compress") compressor_path = "."
if platform.system() == 'Windows': if platform.system() == 'Windows':
executable_path = "Compress.exe" executable_path = "Compress.exe"

View File

@@ -140,6 +140,11 @@ def set_rules(ootworld):
location = world.get_location('Sheik in Ice Cavern', player) location = world.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: item.player == player and item.type == 'Song') 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: for name in ootworld.always_hints:
add_rule(world.get_location(name, player), guarantee_hint) 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 .Cosmetics import patch_cosmetics
from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
from .HintList import getRequiredHints from .HintList import getRequiredHints
from .SaveContext import SaveContext
from Utils import get_options, output_path from Utils import get_options, output_path
from BaseClasses import MultiWorld, CollectionState, RegionType 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.mq_dungeons_random = False # this will be a deprecated option later
self.ocarina_songs = False # just need to pull in the OcarinaSongs module 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.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 # Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] 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) self.remove_from_start_inventory.remove(item.name)
removed_items.append(item.name) removed_items.append(item.name)
else: else:
self.starting_items[item.name] += 1 if item.name not in SaveContext.giveable_items:
if item.type == 'Song': raise Exception(f"Invalid OoT starting item: {item.name}")
self.starting_songs = True else:
# Call the junk fill and get a replacement self.starting_items[item.name] += 1
if item in self.itempool: if item.type == 'Song':
self.itempool.remove(item) self.starting_songs = True
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) # 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: if self.start_with_consumables:
self.starting_items['Deku Sticks'] = 30 self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40 self.starting_items['Deku Nuts'] = 40
@@ -716,7 +722,6 @@ class OOTWorld(World):
impa = self.world.get_location("Song from Impa", self.player) impa = self.world.get_location("Song from Impa", self.player)
if self.skip_child_zelda: if self.skip_child_zelda:
if impa.item is None: 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_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)) item.player == self.player and item.name in SaveContext.giveable_items))
impa.place_locked_item(item_to_place) 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'])): 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) 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: if loc.player in barren_hint_players:
hint_area = get_hint_area(loc) hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1 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 itemManager: ItemManager
locations = {} locations = {}
hint_blacklist = {'Nothing', 'NoEnergy'}
Logic.factory('vanilla') Logic.factory('vanilla')
@@ -85,6 +86,7 @@ class SMWorld(World):
# keeps Nothing items local so no player will ever pickup Nothing # 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 # 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('Nothing')
self.world.local_items[self.player].value.add('NoEnergy')
if (self.variaRando.args.morphPlacement == "early"): if (self.variaRando.args.morphPlacement == "early"):
self.world.local_items[self.player].value.add('Morph') self.world.local_items[self.player].value.add('Morph')
@@ -126,7 +128,7 @@ class SMWorld(World):
weaponCount[2] += 1 weaponCount[2] += 1
else: else:
isAdvancement = False isAdvancement = False
elif item.Type == 'Nothing': elif item.Category == 'Nothing':
isAdvancement = False isAdvancement = False
itemClass = ItemManager.Items[item.Type].Class itemClass = ItemManager.Items[item.Type].Class

View File

@@ -16,6 +16,7 @@ from utils.doorsmanager import DoorsManager
from logic.logic import Logic from logic.logic import Logic
import utils.log import utils.log
from worlds.sm.Options import StartLocation
# we need to know the logic before doing anything else # we need to know the logic before doing anything else
def getLogic(): def getLogic():
@@ -498,10 +499,12 @@ class VariaRandomizer:
sys.exit(-1) sys.exit(-1)
args.startLocation = random.choice(possibleStartAPs) args.startLocation = random.choice(possibleStartAPs)
elif args.startLocation not in possibleStartAPs: elif args.startLocation not in possibleStartAPs:
optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) args.startLocation = 'Landing Site'
optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) world.start_location[player] = StartLocation(StartLocation.default)
dumpErrorMsgs(args.output, optErrMsgs) #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
sys.exit(-1) #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
#dumpErrorMsgs(args.output, optErrMsgs)
#sys.exit(-1)
ap = getAccessPoint(args.startLocation) ap = getAccessPoint(args.startLocation)
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True: if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location") 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 rule: Callable = lambda state: True
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: 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] = [ location_table: List[LocationData] = [
# PresentItemLocations # PresentItemLocations
LocationData('Tutorial', 'Yo Momma 1', 1337000), 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('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', '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', 'Close combat room', 1337059),
LocationData('Military Fortress', 'Military soldiers bridge', 1337060), LocationData('Military Fortress (hangar)', 'Military soldiers bridge', 1337060),
LocationData('Military Fortress', 'Military giantess room', 1337061), LocationData('Military Fortress (hangar)', 'Military giantess room', 1337061),
LocationData('Military Fortress', 'Military giantess bridge', 1337062), LocationData('Military Fortress (hangar)', '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 (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', '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 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 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', 'Coffee break', 1337066),
LocationData('The lab', 'Lower trash right', 1337067, lambda state: state._timespinner_has_doublejump(world, player)), 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)), 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', 'Dad\'s courtyard chest', 1337079, lambda state: state._timespinner_has_upwarddash(world, player)),
LocationData('Emperors tower', 'Galactic sage room', 1337080), LocationData('Emperors tower', 'Galactic sage room', 1337080),
LocationData('Emperors tower', 'Bottom of Dad\'s right tower', 1337081), 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 left tower balcony', 1337083),
LocationData('Emperors tower', 'Dad\'s Chambers chest', 1337084), LocationData('Emperors tower', 'Dad\'s Chambers chest', 1337084),
LocationData('Emperors tower', 'Dad\'s Chambers pedestal', 1337085), 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 pedestal', 1337154),
LocationData('Royal towers (upper)', 'Aelana\'s chest', 1337155), LocationData('Royal towers (upper)', 'Aelana\'s chest', 1337155),
# 1337176 - 1337176 Cantoran #AncientPyramidLocations
# 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
LocationData('Ancient Pyramid (left)', 'Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Why not it\'s right there', 1337246),
LocationData('Ancient Pyramid (left)', 'Conviction guarded room', 1337247), 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)), LocationData('Ancient Pyramid (right)', 'Pit secret room', 1337248, lambda state: state._timespinner_can_break_walls(world, player)),
@@ -200,47 +191,74 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId) LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId)
] ]
downloadable_locations: Tuple[LocationData, ...] = ( # 1337156 - 1337170 Downloads
# DownloadTerminals if not world or is_option_enabled(world, player, "DownloadableItems"):
LocationData('Library', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)), location_table += (
LocationData('Library', 'Library terminal 2', 1337156, 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 1', 1337157, lambda state: state.has('Tablet', player)),
LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)), # 1337158 Is Lost in time
LocationData('Library', 'V terminal 1', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
LocationData('Library', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'V terminal 1', 1337160, 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', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)), LocationData('Library', 'V terminal 3', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)), LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)),
LocationData('The lab (upper)', 'Chest and download terminal', 1337165, 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 (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)), LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)),
LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)), LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)),
LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)), LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)),
LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)), LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)),
LocationData('The lab (power off)', 'Lab terminal right', 1337170, 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, ...] = ( # 1337176 - 1337176 Cantoran
LocationData('The lab (upper)', 'Ravenlord post fight (pedestal)', 1337237, lambda state: state.has('Merchant Crow', player)), if not world or is_option_enabled(world, player, "Cantoran"):
LocationData('Library top', 'Ifrit post fight (pedestal)', 1337238, lambda state: state.has('Kobo', player)), location_table += (
LocationData('The lab (upper)', 'Ravenlord pre fight', 1337242, lambda state: state.has('Merchant Crow', player)), LocationData('Left Side forest Caves', 'Cantoran', 1337176),
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)),
)
cantoran_locations: Tuple[LocationData, ...] = ( # 1337177 - 1337198 Lore Checks
LocationData('Left Side forest Caves', 'Cantoran', 1337176), 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: # 1337199 - 1337236 Reserved for future use
return ( *location_table, *downloadable_locations, *gyre_archives_locations, *cantoran_locations )
if is_option_enabled(world, player, "DownloadableItems"): # 1337237 - 1337245 GyreArchives
location_table.extend(downloadable_locations) if not world or is_option_enabled(world, player, "GyreArchives"):
if is_option_enabled(world, player, "GyreArchives"): location_table += (
location_table.extend(gyre_archives_locations) LocationData('Ravenlord\'s Lair', 'Ravenlord post fight (pedestal)', 1337237),
if is_option_enabled(world, player, "Cantoran"): LocationData('Ifrit\'s Lair', 'Ifrit post fight (pedestal)', 1337238),
location_table.extend(cantoran_locations) 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) return tuple(location_table)

View File

@@ -50,6 +50,10 @@ class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room" "Cantoran's fight and check are available upon revisiting his room"
display_name = "Cantoran" display_name = "Cantoran"
class LoreChecks(Toggle):
"Memories and journal entries contain items."
display_name = "Lore Checks"
class DamageRando(Toggle): class DamageRando(Toggle):
"Each orb has a high chance of having lower base damage and a low chance of having much higher base damage." "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" display_name = "Damage Rando"
@@ -68,6 +72,7 @@ timespinner_options: Dict[str, Toggle] = {
#"StinkyMaw": StinkyMaw, #"StinkyMaw": StinkyMaw,
"GyreArchives": GyreArchives, "GyreArchives": GyreArchives,
"Cantoran": Cantoran, "Cantoran": Cantoran,
"LoreChecks": LoreChecks,
"DamageRando": DamageRando, "DamageRando": DamageRando,
"DeathLink": DeathLink, "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, 'Lower lake desolation'),
create_region(world, player, locations_per_region, location_cache, 'Library'), 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, '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 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 (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 (lower)'),
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'), 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, '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'),
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'),
create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'), 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, '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, 'Emperors tower'),
create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'), create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'),
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'), 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 (lower)'),
create_region(world, player, locations_per_region, location_cache, 'Royal towers'), 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, '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 (left)'),
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'), create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'),
create_region(world, player, locations_per_region, location_cache, 'Space time continuum') 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', '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', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
connect(world, player, names, 'Library top', 'Library') 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', '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 (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)) 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)', '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, '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', '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', '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', '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')
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 (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)', '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)', '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, '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, 'Emperors tower', 'The lab (upper)')
connect(world, player, names, 'Skeleton Shaft', 'Lake desolation') 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)) 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" game = "Timespinner"
topology_present = True topology_present = True
remote_items = False remote_items = False
data_version = 4 data_version = 6
item_name_to_id = {name: data.code for name, data in item_table.items()} 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)} location_name_to_id = {location.name: location.code for location in get_locations(None, None)}