Merge branch 'main' into docs_consolidation
# Conflicts: # WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md # WebHostLib/static/assets/tutorial/archipelago/plando_en.md # WebHostLib/static/assets/tutorial/archipelago/triggers_en.md # WebHostLib/static/assets/tutorial/timespinner/setup_en.md
This commit is contained in:
commit
9599f54b06
|
@ -191,8 +191,6 @@ def main(args=None, callback=ERmain):
|
||||||
if len(player_settings.values()) > 1:
|
if len(player_settings.values()) > 1:
|
||||||
important[option] = {player: value for player, value in player_settings.items() if
|
important[option] = {player: value for player, value in player_settings.items() if
|
||||||
player <= args.yaml_output}
|
player <= args.yaml_output}
|
||||||
elif len(player_settings.values()) > 0:
|
|
||||||
important[option] = player_settings[1]
|
|
||||||
else:
|
else:
|
||||||
logging.debug(f"No player settings defined for option '{option}'")
|
logging.debug(f"No player settings defined for option '{option}'")
|
||||||
|
|
||||||
|
|
2
Main.py
2
Main.py
|
@ -258,7 +258,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||||
precollected_items = {player: [item.code for item in world_precollected]
|
precollected_items = {player: [item.code for item in world_precollected]
|
||||||
for player, world_precollected in world.precollected_items.items()}
|
for player, world_precollected in world.precollected_items.items()}
|
||||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||||
# for now special case Factorio tech_tree_information
|
|
||||||
sending_visible_players = set()
|
sending_visible_players = set()
|
||||||
|
|
||||||
for slot in world.player_ids:
|
for slot in world.player_ids:
|
||||||
|
|
|
@ -543,7 +543,10 @@ async def on_client_joined(ctx: Context, client: Client):
|
||||||
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
|
||||||
f"{verb} {ctx.games[client.slot]} has joined. "
|
f"{verb} {ctx.games[client.slot]} has joined. "
|
||||||
f"Client({version_str}), {client.tags}).")
|
f"Client({version_str}), {client.tags}).")
|
||||||
|
ctx.notify_client(client, "Now that you are connected, "
|
||||||
|
"you can use !help to list commands to run via the server."
|
||||||
|
"If your client supports it, "
|
||||||
|
"you may have additional local commands you can list with /help.")
|
||||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
@ -639,7 +642,7 @@ def collect_player(ctx: Context, team: int, slot: int):
|
||||||
|
|
||||||
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
|
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
|
||||||
for source_player, location_ids in all_locations.items():
|
for source_player, location_ids in all_locations.items():
|
||||||
register_location_checks(ctx, team, source_player, location_ids)
|
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
|
||||||
update_checked_locations(ctx, team, source_player)
|
update_checked_locations(ctx, team, source_player)
|
||||||
|
|
||||||
|
|
||||||
|
@ -651,10 +654,12 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
return sorted(items)
|
return sorted(items)
|
||||||
|
|
||||||
|
|
||||||
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
|
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
|
||||||
|
count_activity: bool = True):
|
||||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||||
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
|
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
|
||||||
if new_locations:
|
if new_locations:
|
||||||
|
if count_activity:
|
||||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
for location in new_locations:
|
for location in new_locations:
|
||||||
item_id, target_player = ctx.locations[slot][location]
|
item_id, target_player = ctx.locations[slot][location]
|
||||||
|
@ -1086,7 +1091,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output("Cheating is disabled.")
|
self.output("Cheating is disabled.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
|
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
|
||||||
points_available = get_client_points(self.ctx, self.client)
|
points_available = get_client_points(self.ctx, self.client)
|
||||||
if not input_text:
|
if not input_text:
|
||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
|
@ -1098,20 +1103,21 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||||
item_name, usable, response = get_intended_text(input_text,
|
names = world.location_names if for_location else world.all_item_and_group_names
|
||||||
world.all_names if not explicit_location else world.location_names)
|
hint_name, usable, response = get_intended_text(input_text,
|
||||||
|
names)
|
||||||
if usable:
|
if usable:
|
||||||
if item_name in world.hint_blacklist:
|
if hint_name in world.hint_blacklist:
|
||||||
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
hints = []
|
||||||
elif item_name in world.item_name_groups and not explicit_location:
|
elif not for_location and hint_name in world.item_name_groups: # item group name
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item_name]:
|
for item in world.item_name_groups[hint_name]:
|
||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||||
elif item_name in world.item_names and not explicit_location: # item name
|
elif not for_location and hint_name in world.item_names: # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||||
if hints:
|
if hints:
|
||||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||||
|
@ -1173,8 +1179,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||||
"""Use !hint {item_name/location_name},
|
"""Use !hint {item_name},
|
||||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
for example !hint Lamp to get a spoiler peek for that item.
|
||||||
If hint costs are on, this will only give you one new result,
|
If hint costs are on, this will only give you one new result,
|
||||||
you can rerun the command to get more in that case."""
|
you can rerun the command to get more in that case."""
|
||||||
return self.get_hints(item_or_location)
|
return self.get_hints(item_or_location)
|
||||||
|
@ -1509,22 +1515,43 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_hint(self, player_name: str, *item_or_location: str) -> bool:
|
def _cmd_hint(self, player_name: str, *item: str) -> bool:
|
||||||
"""Send out a hint for a player's item or location to their team"""
|
"""Send out a hint for a player's item to their team"""
|
||||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(item_or_location)
|
item = " ".join(item)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
world = proxy_worlds[self.ctx.games[slot]]
|
||||||
item, usable, response = get_intended_text(item, world.all_names)
|
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
|
||||||
if usable:
|
if usable:
|
||||||
if item in world.item_name_groups:
|
if item in world.item_name_groups:
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item]:
|
for item in world.item_name_groups[item]:
|
||||||
hints.extend(collect_hints(self.ctx, team, slot, item))
|
hints.extend(collect_hints(self.ctx, team, slot, item))
|
||||||
elif item in world.item_names: # item name
|
else: # item name
|
||||||
hints = collect_hints(self.ctx, team, slot, item)
|
hints = collect_hints(self.ctx, team, slot, item)
|
||||||
else: # location name
|
if hints:
|
||||||
|
notify_hints(self.ctx, team, hints)
|
||||||
|
else:
|
||||||
|
self.output("No hints found.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.output(response)
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.output(response)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
|
||||||
|
"""Send out a hint for a player's location to their team"""
|
||||||
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
|
if usable:
|
||||||
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
|
item = " ".join(location)
|
||||||
|
world = proxy_worlds[self.ctx.games[slot]]
|
||||||
|
item, usable, response = get_intended_text(item, world.location_names)
|
||||||
|
if usable:
|
||||||
hints = collect_hints_location(self.ctx, team, slot, item)
|
hints = collect_hints_location(self.ctx, team, slot, item)
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
notify_hints(self.ctx, team, hints)
|
||||||
|
|
38
Options.py
38
Options.py
|
@ -210,6 +210,23 @@ class Range(Option, int):
|
||||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
|
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
|
||||||
elif text == "random-middle":
|
elif text == "random-middle":
|
||||||
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
|
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
|
||||||
|
elif text.startswith("random-range-"):
|
||||||
|
textsplit = text.split("-")
|
||||||
|
try:
|
||||||
|
randomrange = [int(textsplit[len(textsplit)-2]), int(textsplit[len(textsplit)-1])]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||||
|
randomrange.sort()
|
||||||
|
if randomrange[0] < cls.range_start or randomrange[1] > cls.range_end:
|
||||||
|
raise Exception(f"{randomrange[0]}-{randomrange[1]} is outside allowed range {cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||||
|
if text.startswith("random-range-low"):
|
||||||
|
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[0]))))
|
||||||
|
elif text.startswith("random-range-middle"):
|
||||||
|
return cls(int(round(random.triangular(randomrange[0], randomrange[1]))))
|
||||||
|
elif text.startswith("random-range-high"):
|
||||||
|
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[1]))))
|
||||||
|
else:
|
||||||
|
return cls(int(round(random.randint(randomrange[0], randomrange[1]))))
|
||||||
else:
|
else:
|
||||||
return cls(random.randint(cls.range_start, cls.range_end))
|
return cls(random.randint(cls.range_start, cls.range_end))
|
||||||
return cls(int(text))
|
return cls(int(text))
|
||||||
|
@ -244,7 +261,22 @@ class OptionNameSet(Option):
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
|
|
||||||
class OptionDict(Option):
|
class VerifyKeys:
|
||||||
|
valid_keys = frozenset()
|
||||||
|
valid_keys_casefold: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify_keys(cls, data):
|
||||||
|
if cls.valid_keys:
|
||||||
|
data = set(data)
|
||||||
|
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||||
|
extra = dataset - cls.valid_keys
|
||||||
|
if extra:
|
||||||
|
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||||
|
f"Allowed keys: {cls.valid_keys}.")
|
||||||
|
|
||||||
|
|
||||||
|
class OptionDict(Option, VerifyKeys):
|
||||||
default = {}
|
default = {}
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
value: typing.Dict[str, typing.Any]
|
value: typing.Dict[str, typing.Any]
|
||||||
|
@ -255,6 +287,7 @@ class OptionDict(Option):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||||
if type(data) == dict:
|
if type(data) == dict:
|
||||||
|
cls.verify_keys(data)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||||
|
@ -276,7 +309,7 @@ class ItemDict(OptionDict):
|
||||||
super(ItemDict, self).__init__(value)
|
super(ItemDict, self).__init__(value)
|
||||||
|
|
||||||
|
|
||||||
class OptionList(Option):
|
class OptionList(Option, VerifyKeys):
|
||||||
default = []
|
default = []
|
||||||
supports_weighting = False
|
supports_weighting = False
|
||||||
value: list
|
value: list
|
||||||
|
@ -292,6 +325,7 @@ class OptionList(Option):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_any(cls, data: typing.Any):
|
def from_any(cls, data: typing.Any):
|
||||||
if type(data) == list:
|
if type(data) == list:
|
||||||
|
cls.verify_keys(data)
|
||||||
return cls(data)
|
return cls(data)
|
||||||
return cls.from_text(str(data))
|
return cls.from_text(str(data))
|
||||||
|
|
||||||
|
|
|
@ -136,8 +136,7 @@ def view_seed(seed: UUID):
|
||||||
seed = Seed.get(id=seed)
|
seed = Seed.get(id=seed)
|
||||||
if not seed:
|
if not seed:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template("viewSeed.html", seed=seed,
|
return render_template("viewSeed.html", seed=seed)
|
||||||
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/new_room/<suuid:seed>')
|
@app.route('/new_room/<suuid:seed>')
|
||||||
|
|
|
@ -65,7 +65,6 @@ def generate_api():
|
||||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
return {"text": "Uncaught Exception:" + str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/status/<suuid:seed>')
|
@api_endpoints.route('/status/<suuid:seed>')
|
||||||
def wait_seed_api(seed: UUID):
|
def wait_seed_api(seed: UUID):
|
||||||
seed_id = seed
|
seed_id = seed
|
||||||
|
|
|
@ -16,7 +16,6 @@ def get_rooms():
|
||||||
"last_port": room.last_port,
|
"last_port": room.last_port,
|
||||||
"timeout": room.timeout,
|
"timeout": room.timeout,
|
||||||
"tracker": room.tracker,
|
"tracker": room.tracker,
|
||||||
"players": room.seed.multidata["names"] if room.seed.multidata else [["Singleplayer"]],
|
|
||||||
})
|
})
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
@ -28,6 +27,6 @@ def get_seeds():
|
||||||
response.append({
|
response.append({
|
||||||
"seed_id": seed.id,
|
"seed_id": seed.id,
|
||||||
"creation_time": seed.creation_time,
|
"creation_time": seed.creation_time,
|
||||||
"players": seed.multidata["names"] if seed.multidata else [["Singleplayer"]],
|
"players": [(slot.player_name, slot.game) for slot in seed.slots],
|
||||||
})
|
})
|
||||||
return jsonify(response)
|
return jsonify(response)
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from Utils import __version__
|
from Utils import __version__
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
@ -9,6 +10,9 @@ import Options
|
||||||
|
|
||||||
target_folder = os.path.join("WebHostLib", "static", "generated")
|
target_folder = os.path.join("WebHostLib", "static", "generated")
|
||||||
|
|
||||||
|
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||||
|
"exclude_locations"}
|
||||||
|
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
|
||||||
|
@ -59,7 +63,10 @@ def create():
|
||||||
|
|
||||||
game_options = {}
|
game_options = {}
|
||||||
for option_name, option in all_options.items():
|
for option_name, option in all_options.items():
|
||||||
if option.options:
|
if option_name in handled_in_js:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif option.options:
|
||||||
game_options[option_name] = this_option = {
|
game_options[option_name] = this_option = {
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||||
|
@ -92,6 +99,32 @@ def create():
|
||||||
"max": option.range_end,
|
"max": option.range_end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elif getattr(option, "verify_item_name", False):
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "items-list",
|
||||||
|
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||||
|
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||||
|
}
|
||||||
|
|
||||||
|
elif getattr(option, "verify_location_name", False):
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "locations-list",
|
||||||
|
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||||
|
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||||
|
}
|
||||||
|
|
||||||
|
elif hasattr(option, "valid_keys"):
|
||||||
|
if option.valid_keys:
|
||||||
|
game_options[option_name] = {
|
||||||
|
"type": "custom-list",
|
||||||
|
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||||
|
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||||
|
"options": list(option.valid_keys),
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.debug(f"{option} not exported to Web Settings.")
|
||||||
|
|
||||||
player_settings["gameOptions"] = game_options
|
player_settings["gameOptions"] = game_options
|
||||||
|
|
||||||
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
||||||
|
|
|
@ -51,10 +51,8 @@ For the above example `nested_option_one` will have `option_one_setting_one` 1 t
|
||||||
so `option_one_setting_one` is guaranteed to occur.
|
so `option_one_setting_one` is guaranteed to occur.
|
||||||
|
|
||||||
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
|
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
|
||||||
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed
|
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
|
||||||
adding more randomness and "mystery" to your settings.
|
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
|
||||||
|
|
||||||
Every configurable setting supports weights.
|
|
||||||
|
|
||||||
### Root Options
|
### Root Options
|
||||||
|
|
||||||
|
@ -114,24 +112,33 @@ See the plando guide for more info on plando options. Plando
|
||||||
guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en)
|
guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en)
|
||||||
|
|
||||||
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
||||||
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
||||||
will give you the item `Rupees(5)` six times, totalling 30 rupees.
|
will give you 30 rupees.
|
||||||
|
|
||||||
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
|
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
|
||||||
the location without using any hint points.
|
the location without using any hint points.
|
||||||
|
|
||||||
* `local_items` will force any items you want to be in your world instead of being in another world.
|
* `local_items` will force any items you want to be in your world instead of being in another world.
|
||||||
|
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located
|
||||||
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be
|
in your own.
|
||||||
located in your own.
|
|
||||||
|
|
||||||
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in
|
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in
|
||||||
it to see how important the location is.
|
it to see how important the location is.
|
||||||
|
|
||||||
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
|
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
|
||||||
item which isn't necessary for progression to go in these locations.
|
item which isn't necessary for progression to go in these locations.
|
||||||
|
|
||||||
### Example YAML
|
### Random numbers
|
||||||
|
|
||||||
|
Options taking a choice of a number can also use a variety of `random` options to choose a number randomly.
|
||||||
|
|
||||||
|
* `random` will choose a number allowed for the setting at random
|
||||||
|
* `random-low` will choose a number allowed for the setting at random, but will be weighted towards lower numbers
|
||||||
|
* `random-middle` will choose a number allowed for the setting at random, but will be weighted towards the middle of the range
|
||||||
|
* `random-high` will choose a number allowed for the setting at random, but will be weighted towards higher numbers
|
||||||
|
* `random-range-#-#` will choose a number at random from between the specified numbers. For example `random-range-40-60`
|
||||||
|
will choose a number between 40 and 60
|
||||||
|
* `random-range-low-#-#`, `random-range-middle-#-#`, and `random-range-high-#-#` will choose a number at random from the
|
||||||
|
specified numbers, but with the specified weights
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
||||||
|
@ -146,6 +153,10 @@ A Link to the Past:
|
||||||
smallkey_shuffle:
|
smallkey_shuffle:
|
||||||
original_dungeon: 1
|
original_dungeon: 1
|
||||||
any_world: 1
|
any_world: 1
|
||||||
|
crystals_needed_for_gt:
|
||||||
|
random-low: 1
|
||||||
|
crystals_needed_for_ganon:
|
||||||
|
random-range-high-1-7: 1
|
||||||
start_inventory:
|
start_inventory:
|
||||||
Pegasus Boots: 1
|
Pegasus Boots: 1
|
||||||
Bombs (3): 2
|
Bombs (3): 2
|
||||||
|
@ -173,46 +184,34 @@ triggers:
|
||||||
```
|
```
|
||||||
|
|
||||||
#### This is a fully functional yaml file that will do all the following things:
|
#### This is a fully functional yaml file that will do all the following things:
|
||||||
|
|
||||||
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
|
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
|
||||||
|
|
||||||
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
|
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
|
||||||
|
|
||||||
* `game` is set to `A Link to the Past` meaning that is what game we will play with this file.
|
* `game` is set to `A Link to the Past` meaning that is what game we will play with this file.
|
||||||
|
|
||||||
* `requires` is set to require release version 0.2.0 or higher.
|
* `requires` is set to require release version 0.2.0 or higher.
|
||||||
|
|
||||||
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
|
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
|
||||||
completely inaccessible but the seed will still be completable.
|
completely inaccessible but the seed will still be completable.
|
||||||
|
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having
|
||||||
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of
|
things to do.
|
||||||
having things to do.
|
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our game `A Link to the Past`.
|
||||||
|
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example
|
||||||
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our
|
we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere
|
||||||
game `A Link to the Past`.
|
amongst the multiworld.
|
||||||
|
* `crystals_needed_for_gt` determines the number of crystals required to enter the Ganon's Tower entrance. In this example
|
||||||
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this
|
a random number will be chosen from the allowed range for this setting (0 through 7) but will be weighted towards a lower number.
|
||||||
example we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be
|
* `crystals_needed_for_ganon` determines the number of crystals required to beat Ganon. In this example a number
|
||||||
placed anywhere amongst the multiworld.
|
between 1 and 7 will be chosen at random, weighted towards a high number.
|
||||||
|
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example
|
||||||
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this
|
we have:
|
||||||
example we have:
|
|
||||||
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||||
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
|
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
|
||||||
|
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use with no cost.
|
||||||
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use
|
|
||||||
with no cost.
|
|
||||||
|
|
||||||
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||||
have to find it ourselves.
|
have to find it ourselves.
|
||||||
|
|
||||||
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||||
|
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld
|
||||||
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
|
that can be used for no cost.
|
||||||
multiworld that can be used for no cost.
|
|
||||||
|
|
||||||
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
|
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
|
||||||
|
|
||||||
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
|
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
|
||||||
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to
|
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world`
|
||||||
the `any_world` result.
|
result.
|
||||||
|
|
|
@ -16,9 +16,15 @@ On the website plando will already be enabled. If you will be generating the gam
|
||||||
enabled (opt-in).
|
enabled (opt-in).
|
||||||
|
|
||||||
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
|
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
|
||||||
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such
|
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as
|
||||||
as
|
`plando_options: bosses, items, texts, connections`.
|
||||||
`plando_options: bosses, items, texts, connections`.
|
* If you are not the one doing the generation or even if you are you can add to the `requires` section of your yaml so that it will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||||
|
```yaml
|
||||||
|
requires:
|
||||||
|
version: current.version.number
|
||||||
|
plando: bosses, items, texts, connections
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Item Plando
|
## Item Plando
|
||||||
|
|
||||||
|
|
|
@ -20,27 +20,19 @@ General plando guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en
|
||||||
Link to the Past plando guide: [LttP Plando Guide](/tutorial/zelda3/plando/en)
|
Link to the Past plando guide: [LttP Plando Guide](/tutorial/zelda3/plando/en)
|
||||||
|
|
||||||
## Trigger use
|
## Trigger use
|
||||||
|
Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the bottom of the yaml for clear organization.
|
||||||
Triggers have to be defined in the root of the yaml file meaning it must be outside a game section. The best place to do
|
- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and
|
||||||
this is the bottom of the yaml.
|
`option_result` from which it will react to and then an `options` section for the definition of what will happen.
|
||||||
|
|
||||||
- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`,
|
|
||||||
and `option_result` from which it will react to and then an `options` section where the definition of what will
|
|
||||||
happen.
|
|
||||||
- `option_category` is the defining section from which the option is defined in.
|
- `option_category` is the defining section from which the option is defined in.
|
||||||
- Example: `A Link to the Past`
|
- Example: `A Link to the Past`
|
||||||
- This is the root category the option is located in. If the option you're triggering off of is in root then you
|
- This is the root category the option is located in. If the option you're triggering off of is in root then you would use `null`, otherwise this is the game for which you want this option trigger to activate.
|
||||||
would use `null`, otherwise this is the game for which you want this option trigger to activate.
|
|
||||||
- `option_name` is the option setting from which the triggered choice is going to react to.
|
- `option_name` is the option setting from which the triggered choice is going to react to.
|
||||||
- Example: `shop_item_slots`
|
- Example: `shop_item_slots`
|
||||||
- This can be any option from any category defined in the yaml file in either root or a game section except
|
- This can be any option from any category defined in the yaml file in either root or a game section.
|
||||||
for `game`.
|
|
||||||
- `option_result` is the result of this option setting from which you would like to react.
|
- `option_result` is the result of this option setting from which you would like to react.
|
||||||
- Example: `15`
|
- Example: `15`
|
||||||
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
|
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple results you would need multiple triggers for this.
|
||||||
results you would need multiple triggers for this.
|
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring another option also gets selected or placing an item in a certain location. It is possible to have multiple things happen in this section.
|
||||||
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
|
|
||||||
another option also gets selected or placing an item in a certain location.
|
|
||||||
- Example:
|
- Example:
|
||||||
```yaml
|
```yaml
|
||||||
A Link to the Past:
|
A Link to the Past:
|
||||||
|
@ -85,5 +77,39 @@ For example:
|
||||||
Timespinner:
|
Timespinner:
|
||||||
Inverted: true
|
Inverted: true
|
||||||
```
|
```
|
||||||
|
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
|
||||||
|
|
||||||
In this example if your world happens to roll `SpecificKeycards` then your game will also start in inverted.
|
It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1 AND setting 2".
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```yaml
|
||||||
|
triggers:
|
||||||
|
- option_category: Secret of Evermore
|
||||||
|
option_name: doggomizer
|
||||||
|
option_result: pupdunk
|
||||||
|
options:
|
||||||
|
Secret of Evermore:
|
||||||
|
difficulty:
|
||||||
|
normal: 50
|
||||||
|
pupdunk_hard: 25
|
||||||
|
pupdunk_mystery: 25
|
||||||
|
exp_modifier:
|
||||||
|
150: 50
|
||||||
|
200: 50
|
||||||
|
- option_category: Secret of Evermore
|
||||||
|
option_name: difficulty
|
||||||
|
option_result: pupdunk_hard
|
||||||
|
options:
|
||||||
|
Secret of Evermore:
|
||||||
|
fix_wings_glitch: false
|
||||||
|
difficulty: hard
|
||||||
|
- option_category: Secret of Evermore
|
||||||
|
option_name: difficulty
|
||||||
|
option_result: pupdunk_mystery
|
||||||
|
options:
|
||||||
|
Secret of Evermore:
|
||||||
|
fix_wings_glitch: false
|
||||||
|
difficulty: mystery
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Timespinner Randomizer Installationsanweisungen
|
||||||
|
|
||||||
|
## Benötigte Software
|
||||||
|
|
||||||
|
- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner) oder [Timespinner (GOG)](https://www.gog.com/game/timespinner) (andere Versionen werden nicht unterstützt)
|
||||||
|
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||||
|
|
||||||
|
## Wie funktioniert's?
|
||||||
|
|
||||||
|
Der Timespinner Randomizer lädt die Timespinner.exe im gleichen Verzeichnis und verändert seine Speicherinformationen um die Randomisierung der Gegenstände zu erlauben
|
||||||
|
|
||||||
|
## Installationsanweisungen
|
||||||
|
|
||||||
|
1. Die aktuellsten Dateien des Randomizers findest du ganz oben auf dieser Webseite: [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases). Lade dir unter 'Assets' die .zip Datei für dein Betriebssystem herunter
|
||||||
|
2. Entpacke die .zip Datei im Ordner, in dem das Spiel Timespinner installiert ist
|
||||||
|
|
||||||
|
## Den Randomizer starten
|
||||||
|
|
||||||
|
- auf Windows: Starte die Datei TsRandomizer.exe
|
||||||
|
- auf Linux: Starte die Datei TsRandomizer.bin.x86_64
|
||||||
|
- auf Mac: Starte die Datei TsRandomizer.bin.osx
|
||||||
|
|
||||||
|
... im Ordner in dem die Inhalte aus der .zip Datei entpackt wurden
|
||||||
|
|
||||||
|
Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
|
||||||
|
|
||||||
|
## An einer Multiworld teilnehmen
|
||||||
|
|
||||||
|
1. Starte den Randomizer wie unter [Den Randomizer starten](#Den-Randomizer-starten) beschrieben
|
||||||
|
2. Wähle "New Game"
|
||||||
|
3. Wechsle "<< Select Seed >>" zu "<< Archiplago >>" indem du "links" auf deinem Controller oder der Tastatur drückst
|
||||||
|
4. Wähle "<< Archipelago >>" um ein neues Menu zu öffnen, wo du deine Logininformationen für Archipelago eingeben kannst
|
||||||
|
* ANMERKUNG: Die Eingabefelder unterstützen das Einfügen von Informationen mittels 'Ctrl + V'
|
||||||
|
5. Wähle "Connect"
|
||||||
|
6. Wenn alles funktioniert hat, wechselt das Spiel zurück zur Auswahl des Schwierigkeitsgrads und das Spiel startet, sobald du einen davon ausgewählt hast
|
||||||
|
|
||||||
|
## Woher bekomme ich eine Konfigurationsdatei?
|
||||||
|
Die [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) Seite auf der Website erlaubt dir, persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
|
||||||
|
|
||||||
|
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an
|
||||||
|
* Die Timespinner Randomizer Optionen "ProgressiveVerticalMovement" & "ProgressiveKeycards" werden in Archipelago Seeds aktuell nicht unterstützt
|
|
@ -2,41 +2,29 @@
|
||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- One of:
|
- [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 (steam) from: [Timespinner Steam Store Page](https://store.steampowered.com/app/368620/Timespinner/)
|
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||||
- Timespinner (drm free) from: [Timespinner Humble Store Page](https://www.humblebundle.com/store/timespinner)
|
|
||||||
- Timespinner Randomizer from: [Timespinner Randomizer GitHub](https://github.com/JarnoWesthof/TsRandomizer)
|
|
||||||
|
|
||||||
## General Concept
|
## General Concept
|
||||||
|
|
||||||
The Timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for
|
The Timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items
|
||||||
randomization of the items.
|
|
||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you
|
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 TsRandomizer.bin.x86_64 (on Linux) or TsRandomizer.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)
|
||||||
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 for TsRandomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
1. Run TsRandomizer.exe
|
1. Run TsRandomizer.exe
|
||||||
2. Select "New Game"
|
2. Select "New Game"
|
||||||
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
|
3. Switch "<< Select Seed >>" to "<< Archipelago >>" by pressing left on your controller or keyboard
|
||||||
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
|
4. Select "<< Archipelago >>" to open a new menu where you can enter your Archipelago login credentials
|
||||||
* NOTE: the input fields support Ctrl + V pasting of values
|
* NOTE: the input fields support Ctrl + V pasting of values
|
||||||
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
|
6. If all went well you will be taken back to the difficulty selection menu and the game will start as soon as you select a difficulty
|
||||||
difficulty
|
|
||||||
|
|
||||||
## Where do I get a config file?
|
## Where do I get a config file?
|
||||||
|
The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to configure your personal settings and export them into a config file
|
||||||
The [Timespinner Player Settings Page](https://archipelago.gg/games/Timespinner/player-settings) on the website allows
|
|
||||||
you to configure your personal settings and export a config file from them.
|
|
||||||
|
|
||||||
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
||||||
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported
|
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds
|
||||||
on Archipelago generated seeds
|
|
||||||
|
|
|
@ -54,7 +54,8 @@
|
||||||
"filename": "archipelago/advanced_settings_en.md",
|
"filename": "archipelago/advanced_settings_en.md",
|
||||||
"link": "archipelago/advanced_settings/en",
|
"link": "archipelago/advanced_settings/en",
|
||||||
"authors": [
|
"authors": [
|
||||||
"alwaysintreble"
|
"alwaysintreble",
|
||||||
|
"Alchav"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -291,6 +292,16 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
"Jarno"
|
"Jarno"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "German",
|
||||||
|
"filename": "timespinner/setup_de.md",
|
||||||
|
"link": "timespinner/setup/de",
|
||||||
|
"authors": [
|
||||||
|
"Grrmo",
|
||||||
|
"Fynxes",
|
||||||
|
"Blaze0168"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,24 +85,30 @@ const createDefaultSettings = (settingData) => {
|
||||||
newSettings[game][gameSetting]['random-low'] = 0;
|
newSettings[game][gameSetting]['random-low'] = 0;
|
||||||
newSettings[game][gameSetting]['random-high'] = 0;
|
newSettings[game][gameSetting]['random-high'] = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'items-list':
|
||||||
|
case 'locations-list':
|
||||||
|
case 'custom-list':
|
||||||
|
newSettings[game][gameSetting] = [];
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
|
console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettings[game].start_inventory = [];
|
newSettings[game].start_inventory = {};
|
||||||
newSettings[game].exclude_locations = [];
|
newSettings[game].exclude_locations = [];
|
||||||
newSettings[game].local_items = [];
|
newSettings[game].local_items = [];
|
||||||
newSettings[game].non_local_items = [];
|
newSettings[game].non_local_items = [];
|
||||||
newSettings[game].start_hints = [];
|
newSettings[game].start_hints = [];
|
||||||
|
newSettings[game].start_location_hints = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
|
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) => {
|
const buildUI = (settingData) => {
|
||||||
// Build the game-choice div
|
// Build the game-choice div
|
||||||
buildGameChoice(settingData.games);
|
buildGameChoice(settingData.games);
|
||||||
|
@ -133,12 +139,17 @@ const buildUI = (settingData) => {
|
||||||
|
|
||||||
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||||
gameDiv.appendChild(itemsDiv);
|
gameDiv.appendChild(itemsDiv);
|
||||||
|
|
||||||
|
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
|
||||||
|
gameDiv.appendChild(hintsDiv);
|
||||||
|
|
||||||
gamesWrapper.appendChild(gameDiv);
|
gamesWrapper.appendChild(gameDiv);
|
||||||
|
|
||||||
collapseButton.addEventListener('click', () => {
|
collapseButton.addEventListener('click', () => {
|
||||||
collapseButton.classList.add('invisible');
|
collapseButton.classList.add('invisible');
|
||||||
weightedSettingsDiv.classList.add('invisible');
|
weightedSettingsDiv.classList.add('invisible');
|
||||||
itemsDiv.classList.add('invisible');
|
itemsDiv.classList.add('invisible');
|
||||||
|
hintsDiv.classList.add('invisible');
|
||||||
expandButton.classList.remove('invisible');
|
expandButton.classList.remove('invisible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -146,6 +157,7 @@ const buildUI = (settingData) => {
|
||||||
collapseButton.classList.remove('invisible');
|
collapseButton.classList.remove('invisible');
|
||||||
weightedSettingsDiv.classList.remove('invisible');
|
weightedSettingsDiv.classList.remove('invisible');
|
||||||
itemsDiv.classList.remove('invisible');
|
itemsDiv.classList.remove('invisible');
|
||||||
|
hintsDiv.classList.remove('invisible');
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -462,6 +474,18 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
settingWrapper.appendChild(rangeTable);
|
settingWrapper.appendChild(rangeTable);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'items-list':
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'locations-list':
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'custom-list':
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`);
|
console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`);
|
||||||
return;
|
return;
|
||||||
|
@ -474,6 +498,9 @@ const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildItemsDiv = (game, items) => {
|
const buildItemsDiv = (game, items) => {
|
||||||
|
// Sort alphabetical, in pace
|
||||||
|
items.sort();
|
||||||
|
|
||||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const itemsDiv = document.createElement('div');
|
const itemsDiv = document.createElement('div');
|
||||||
itemsDiv.classList.add('items-div');
|
itemsDiv.classList.add('items-div');
|
||||||
|
@ -537,19 +564,21 @@ const buildItemsDiv = (game, items) => {
|
||||||
nonLocalItems.addEventListener('drop', itemDropHandler);
|
nonLocalItems.addEventListener('drop', itemDropHandler);
|
||||||
|
|
||||||
// Populate the divs
|
// Populate the divs
|
||||||
items.sort().forEach((item) => {
|
items.forEach((item) => {
|
||||||
const itemDiv = buildItemDiv(game, item);
|
if (Object.keys(currentSettings[game].start_inventory).includes(item)){
|
||||||
|
const itemDiv = buildItemQtyDiv(game, item);
|
||||||
if (currentSettings[game].start_inventory.includes(item)){
|
|
||||||
itemDiv.setAttribute('data-setting', 'start_inventory');
|
itemDiv.setAttribute('data-setting', 'start_inventory');
|
||||||
startInventory.appendChild(itemDiv);
|
startInventory.appendChild(itemDiv);
|
||||||
} else if (currentSettings[game].local_items.includes(item)) {
|
} else if (currentSettings[game].local_items.includes(item)) {
|
||||||
|
const itemDiv = buildItemDiv(game, item);
|
||||||
itemDiv.setAttribute('data-setting', 'local_items');
|
itemDiv.setAttribute('data-setting', 'local_items');
|
||||||
localItems.appendChild(itemDiv);
|
localItems.appendChild(itemDiv);
|
||||||
} else if (currentSettings[game].non_local_items.includes(item)) {
|
} else if (currentSettings[game].non_local_items.includes(item)) {
|
||||||
|
const itemDiv = buildItemDiv(game, item);
|
||||||
itemDiv.setAttribute('data-setting', 'non_local_items');
|
itemDiv.setAttribute('data-setting', 'non_local_items');
|
||||||
nonLocalItems.appendChild(itemDiv);
|
nonLocalItems.appendChild(itemDiv);
|
||||||
} else {
|
} else {
|
||||||
|
const itemDiv = buildItemDiv(game, item);
|
||||||
availableItems.appendChild(itemDiv);
|
availableItems.appendChild(itemDiv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -580,6 +609,39 @@ const buildItemDiv = (game, item) => {
|
||||||
return itemDiv;
|
return itemDiv;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildItemQtyDiv = (game, item) => {
|
||||||
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
const itemQtyDiv = document.createElement('div');
|
||||||
|
itemQtyDiv.classList.add('item-qty-div');
|
||||||
|
itemQtyDiv.setAttribute('id', `${game}-${item}`);
|
||||||
|
itemQtyDiv.setAttribute('data-game', game);
|
||||||
|
itemQtyDiv.setAttribute('data-item', item);
|
||||||
|
itemQtyDiv.setAttribute('draggable', 'true');
|
||||||
|
itemQtyDiv.innerText = item;
|
||||||
|
|
||||||
|
const inputWrapper = document.createElement('div');
|
||||||
|
inputWrapper.classList.add('item-qty-input-wrapper')
|
||||||
|
|
||||||
|
const itemQty = document.createElement('input');
|
||||||
|
itemQty.setAttribute('value', currentSettings[game].start_inventory.hasOwnProperty(item) ?
|
||||||
|
currentSettings[game].start_inventory[item] : '1');
|
||||||
|
itemQty.setAttribute('data-game', game);
|
||||||
|
itemQty.setAttribute('data-setting', 'start_inventory');
|
||||||
|
itemQty.setAttribute('data-option', item);
|
||||||
|
itemQty.setAttribute('maxlength', '3');
|
||||||
|
itemQty.addEventListener('keyup', (evt) => {
|
||||||
|
evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value);
|
||||||
|
updateItemSetting(evt);
|
||||||
|
});
|
||||||
|
inputWrapper.appendChild(itemQty);
|
||||||
|
itemQtyDiv.appendChild(inputWrapper);
|
||||||
|
|
||||||
|
itemQtyDiv.addEventListener('dragstart', (evt) => {
|
||||||
|
evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
|
||||||
|
});
|
||||||
|
return itemQtyDiv;
|
||||||
|
};
|
||||||
|
|
||||||
const itemDragoverHandler = (evt) => {
|
const itemDragoverHandler = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
};
|
};
|
||||||
|
@ -592,23 +654,34 @@ const itemDropHandler = (evt) => {
|
||||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const game = sourceDiv.getAttribute('data-game');
|
const game = sourceDiv.getAttribute('data-game');
|
||||||
const item = sourceDiv.getAttribute('data-item');
|
const item = sourceDiv.getAttribute('data-item');
|
||||||
const itemDiv = buildItemDiv(game, item);
|
|
||||||
|
|
||||||
const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
|
const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
|
||||||
const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
|
const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
|
||||||
|
|
||||||
|
const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, item);
|
||||||
|
|
||||||
if (oldSetting) {
|
if (oldSetting) {
|
||||||
|
if (oldSetting === 'start_inventory') {
|
||||||
|
if (currentSettings[game][oldSetting].hasOwnProperty(item)) {
|
||||||
|
delete currentSettings[game][oldSetting][item];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (currentSettings[game][oldSetting].includes(item)) {
|
if (currentSettings[game][oldSetting].includes(item)) {
|
||||||
currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1);
|
currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newSetting) {
|
if (newSetting) {
|
||||||
itemDiv.setAttribute('data-setting', newSetting);
|
itemDiv.setAttribute('data-setting', newSetting);
|
||||||
document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv);
|
document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv);
|
||||||
|
if (newSetting === 'start_inventory') {
|
||||||
|
currentSettings[game][newSetting][item] = 1;
|
||||||
|
} else {
|
||||||
if (!currentSettings[game][newSetting].includes(item)){
|
if (!currentSettings[game][newSetting].includes(item)){
|
||||||
currentSettings[game][newSetting].push(item);
|
currentSettings[game][newSetting].push(item);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No setting was assigned, this item has been removed from the settings
|
// No setting was assigned, this item has been removed from the settings
|
||||||
document.getElementById(`${game}-available_items`).appendChild(itemDiv);
|
document.getElementById(`${game}-available_items`).appendChild(itemDiv);
|
||||||
|
@ -621,6 +694,157 @@ const itemDropHandler = (evt) => {
|
||||||
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
|
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildHintsDiv = (game, items, locations) => {
|
||||||
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
|
||||||
|
// Sort alphabetical, in place
|
||||||
|
items.sort();
|
||||||
|
locations.sort();
|
||||||
|
|
||||||
|
const hintsDiv = document.createElement('div');
|
||||||
|
hintsDiv.classList.add('hints-div');
|
||||||
|
const hintsHeader = document.createElement('h3');
|
||||||
|
hintsHeader.innerText = 'Item & Location Hints';
|
||||||
|
hintsDiv.appendChild(hintsHeader);
|
||||||
|
const hintsDescription = document.createElement('p');
|
||||||
|
hintsDescription.classList.add('setting-description');
|
||||||
|
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
|
||||||
|
' items are, or what those locations contain. Excluded locations will not contain progression items.';
|
||||||
|
hintsDiv.appendChild(hintsDescription);
|
||||||
|
|
||||||
|
const itemHintsContainer = document.createElement('div');
|
||||||
|
itemHintsContainer.classList.add('hints-container');
|
||||||
|
|
||||||
|
const itemHintsWrapper = document.createElement('div');
|
||||||
|
itemHintsWrapper.classList.add('hints-wrapper');
|
||||||
|
itemHintsWrapper.innerText = 'Starting Item Hints';
|
||||||
|
|
||||||
|
const itemHintsDiv = document.createElement('div');
|
||||||
|
itemHintsDiv.classList.add('item-container');
|
||||||
|
items.forEach((item) => {
|
||||||
|
const itemDiv = document.createElement('div');
|
||||||
|
itemDiv.classList.add('hint-div');
|
||||||
|
|
||||||
|
const itemLabel = document.createElement('label');
|
||||||
|
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
|
||||||
|
|
||||||
|
const itemCheckbox = document.createElement('input');
|
||||||
|
itemCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
itemCheckbox.setAttribute('id', `${game}-start_hints-${item}`);
|
||||||
|
itemCheckbox.setAttribute('data-game', game);
|
||||||
|
itemCheckbox.setAttribute('data-setting', 'start_hints');
|
||||||
|
itemCheckbox.setAttribute('data-option', item);
|
||||||
|
if (currentSettings[game].start_hints.includes(item)) {
|
||||||
|
itemCheckbox.setAttribute('checked', 'true');
|
||||||
|
}
|
||||||
|
itemCheckbox.addEventListener('change', hintChangeHandler);
|
||||||
|
itemLabel.appendChild(itemCheckbox);
|
||||||
|
|
||||||
|
const itemName = document.createElement('span');
|
||||||
|
itemName.innerText = item;
|
||||||
|
itemLabel.appendChild(itemName);
|
||||||
|
|
||||||
|
itemDiv.appendChild(itemLabel);
|
||||||
|
itemHintsDiv.appendChild(itemDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
itemHintsWrapper.appendChild(itemHintsDiv);
|
||||||
|
itemHintsContainer.appendChild(itemHintsWrapper);
|
||||||
|
|
||||||
|
const locationHintsWrapper = document.createElement('div');
|
||||||
|
locationHintsWrapper.classList.add('hints-wrapper');
|
||||||
|
locationHintsWrapper.innerText = 'Starting Location Hints';
|
||||||
|
|
||||||
|
const locationHintsDiv = document.createElement('div');
|
||||||
|
locationHintsDiv.classList.add('item-container');
|
||||||
|
locations.forEach((location) => {
|
||||||
|
const locationDiv = document.createElement('div');
|
||||||
|
locationDiv.classList.add('hint-div');
|
||||||
|
|
||||||
|
const locationLabel = document.createElement('label');
|
||||||
|
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
|
||||||
|
|
||||||
|
const locationCheckbox = document.createElement('input');
|
||||||
|
locationCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
locationCheckbox.setAttribute('id', `${game}-start_location_hints-${location}`);
|
||||||
|
locationCheckbox.setAttribute('data-game', game);
|
||||||
|
locationCheckbox.setAttribute('data-setting', 'start_location_hints');
|
||||||
|
locationCheckbox.setAttribute('data-option', location);
|
||||||
|
if (currentSettings[game].start_location_hints.includes(location)) {
|
||||||
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||||
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
|
||||||
|
const locationName = document.createElement('span');
|
||||||
|
locationName.innerText = location;
|
||||||
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
|
locationDiv.appendChild(locationLabel);
|
||||||
|
locationHintsDiv.appendChild(locationDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
locationHintsWrapper.appendChild(locationHintsDiv);
|
||||||
|
itemHintsContainer.appendChild(locationHintsWrapper);
|
||||||
|
|
||||||
|
const excludeLocationsWrapper = document.createElement('div');
|
||||||
|
excludeLocationsWrapper.classList.add('hints-wrapper');
|
||||||
|
excludeLocationsWrapper.innerText = 'Exclude Locations';
|
||||||
|
|
||||||
|
const excludeLocationsDiv = document.createElement('div');
|
||||||
|
excludeLocationsDiv.classList.add('item-container');
|
||||||
|
locations.forEach((location) => {
|
||||||
|
const locationDiv = document.createElement('div');
|
||||||
|
locationDiv.classList.add('hint-div');
|
||||||
|
|
||||||
|
const locationLabel = document.createElement('label');
|
||||||
|
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
|
||||||
|
|
||||||
|
const locationCheckbox = document.createElement('input');
|
||||||
|
locationCheckbox.setAttribute('type', 'checkbox');
|
||||||
|
locationCheckbox.setAttribute('id', `${game}-exclude_locations-${location}`);
|
||||||
|
locationCheckbox.setAttribute('data-game', game);
|
||||||
|
locationCheckbox.setAttribute('data-setting', 'exclude_locations');
|
||||||
|
locationCheckbox.setAttribute('data-option', location);
|
||||||
|
if (currentSettings[game].exclude_locations.includes(location)) {
|
||||||
|
locationCheckbox.setAttribute('checked', '1');
|
||||||
|
}
|
||||||
|
locationCheckbox.addEventListener('change', hintChangeHandler);
|
||||||
|
locationLabel.appendChild(locationCheckbox);
|
||||||
|
|
||||||
|
const locationName = document.createElement('span');
|
||||||
|
locationName.innerText = location;
|
||||||
|
locationLabel.appendChild(locationName);
|
||||||
|
|
||||||
|
locationDiv.appendChild(locationLabel);
|
||||||
|
excludeLocationsDiv.appendChild(locationDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
|
||||||
|
itemHintsContainer.appendChild(excludeLocationsWrapper);
|
||||||
|
|
||||||
|
hintsDiv.appendChild(itemHintsContainer);
|
||||||
|
return hintsDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hintChangeHandler = (evt) => {
|
||||||
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
const game = evt.target.getAttribute('data-game');
|
||||||
|
const setting = evt.target.getAttribute('data-setting');
|
||||||
|
const option = evt.target.getAttribute('data-option');
|
||||||
|
|
||||||
|
if (evt.target.checked) {
|
||||||
|
if (!currentSettings[game][setting].includes(option)) {
|
||||||
|
currentSettings[game][setting].push(option);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentSettings[game][setting].includes(option)) {
|
||||||
|
currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
|
||||||
|
};
|
||||||
|
|
||||||
const updateVisibleGames = () => {
|
const updateVisibleGames = () => {
|
||||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
Object.keys(settings.game).forEach((game) => {
|
Object.keys(settings.game).forEach((game) => {
|
||||||
|
@ -664,25 +888,44 @@ const updateBaseSetting = (event) => {
|
||||||
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
localStorage.setItem('weighted-settings', JSON.stringify(settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateGameSetting = (event) => {
|
const updateGameSetting = (evt) => {
|
||||||
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const game = event.target.getAttribute('data-game');
|
const game = evt.target.getAttribute('data-game');
|
||||||
const setting = event.target.getAttribute('data-setting');
|
const setting = evt.target.getAttribute('data-setting');
|
||||||
const option = event.target.getAttribute('data-option');
|
const option = evt.target.getAttribute('data-option');
|
||||||
const type = event.target.getAttribute('data-type');
|
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
|
||||||
document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value;
|
options[game][setting][option] = isNaN(evt.target.value) ?
|
||||||
options[game][setting][option] = isNaN(event.target.value) ?
|
evt.target.value : parseInt(evt.target.value, 10);
|
||||||
event.target.value : parseInt(event.target.value, 10);
|
|
||||||
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportSettings = () => {
|
const updateItemSetting = (evt) => {
|
||||||
|
const options = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
|
const game = evt.target.getAttribute('data-game');
|
||||||
|
const setting = evt.target.getAttribute('data-setting');
|
||||||
|
const option = evt.target.getAttribute('data-option');
|
||||||
|
if (setting === 'start_inventory') {
|
||||||
|
options[game][setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0;
|
||||||
|
} else {
|
||||||
|
options[game][setting][option] = isNaN(evt.target.value) ?
|
||||||
|
evt.target.value : parseInt(evt.target.value, 10);
|
||||||
|
}
|
||||||
|
localStorage.setItem('weighted-settings', JSON.stringify(options));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSettings = () => {
|
||||||
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
|
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');
|
const userMessage = document.getElementById('user-message');
|
||||||
|
let errorMessage = null;
|
||||||
|
|
||||||
|
// User must choose a name for their file
|
||||||
|
if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') {
|
||||||
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
|
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
|
||||||
userMessage.classList.add('visible');
|
userMessage.classList.add('visible');
|
||||||
window.scrollTo(0, 0);
|
userMessage.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -702,9 +945,41 @@ const exportSettings = () => {
|
||||||
delete settings[game][setting][option];
|
delete settings[game][setting][option];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(settings[game][setting]).length === 0 &&
|
||||||
|
!Array.isArray(settings[game][setting]) &&
|
||||||
|
setting !== 'start_inventory'
|
||||||
|
) {
|
||||||
|
errorMessage = `${game} // ${setting} has no values above zero!`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (Object.keys(settings.game).length === 0) {
|
||||||
|
errorMessage = 'You have not chosen a game to play!';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error occurred, alert the user and do not export the file
|
||||||
|
if (errorMessage) {
|
||||||
|
userMessage.innerText = errorMessage;
|
||||||
|
userMessage.classList.add('visible');
|
||||||
|
userMessage.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no error occurred, hide the user message if it is visible
|
||||||
|
userMessage.classList.remove('visible');
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportSettings = () => {
|
||||||
|
const settings = validateSettings();
|
||||||
|
if (!settings) { return; }
|
||||||
|
|
||||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||||
};
|
};
|
||||||
|
@ -721,9 +996,12 @@ const download = (filename, text) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateGame = (raceMode = false) => {
|
const generateGame = (raceMode = false) => {
|
||||||
|
const settings = validateSettings();
|
||||||
|
if (!settings) { return; }
|
||||||
|
|
||||||
axios.post('/api/generate', {
|
axios.post('/api/generate', {
|
||||||
weights: { player: localStorage.getItem('weighted-settings') },
|
weights: { player: JSON.stringify(settings) },
|
||||||
presetData: { player: localStorage.getItem('weighted-settings') },
|
presetData: { player: JSON.stringify(settings) },
|
||||||
playerCount: 1,
|
playerCount: 1,
|
||||||
race: raceMode ? '1' : '0',
|
race: raceMode ? '1' : '0',
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
|
@ -735,7 +1013,10 @@ const generateGame = (raceMode = false) => {
|
||||||
userMessage.innerText += ' ' + error.response.data.text;
|
userMessage.innerText += ' ' + error.response.data.text;
|
||||||
}
|
}
|
||||||
userMessage.classList.add('visible');
|
userMessage.classList.add('visible');
|
||||||
window.scrollTo(0, 0);
|
userMessage.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -102,26 +102,90 @@ html{
|
||||||
|
|
||||||
#weighted-settings .items-wrapper .item-set-wrapper{
|
#weighted-settings .items-wrapper .item-set-wrapper{
|
||||||
width: 24%;
|
width: 24%;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .items-wrapper .item-container{
|
#weighted-settings .item-container{
|
||||||
border: 1px solid #ffffff;
|
border: 1px solid #ffffff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .items-wrapper .item-container .item-div{
|
#weighted-settings .item-container .item-div{
|
||||||
padding: 0.15rem;
|
padding: 0.125rem 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#weighted-settings .items-wrapper .item-container .item-div:hover{
|
#weighted-settings .item-container .item-div:hover{
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div input{
|
||||||
|
min-width: unset;
|
||||||
|
width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .item-container .item-qty-div:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div{
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div h3{
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div .hints-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div .hints-wrapper{
|
||||||
|
width: 32.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div .hints-wrapper .hint-div{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div .hints-wrapper .hint-div:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .hints-div .hints-wrapper .hint-div label{
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#weighted-settings #weighted-settings-button-row{
|
#weighted-settings #weighted-settings-button-row{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -43,7 +43,6 @@
|
||||||
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
|
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
|
||||||
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
|
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
|
||||||
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
|
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
|
||||||
<td><img src="{{ icons['Dragon Head'] }}" class="{{ 'acquired' if game_finished }}" title="Ender Dragon" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table id="location-table">
|
<table id="location-table">
|
||||||
|
|
|
@ -18,10 +18,13 @@
|
||||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
A more advanced settings configuration for all games can be found on the
|
||||||
|
<a href="/weighted-settings">Weighted Settings</a> page.
|
||||||
<br />
|
<br />
|
||||||
Advanced users can download a template file for this game
|
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||||
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
|
<br />
|
||||||
|
You may also download the
|
||||||
|
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||||
|
|
|
@ -31,12 +31,16 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rooms: </td>
|
<td>Rooms: </td>
|
||||||
<td>
|
<td>
|
||||||
{% call macros.list_rooms(rooms) %}
|
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||||
</li>
|
</li>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
{% if seed.rooms %}<span>
|
||||||
|
There are a total of {{ seed.rooms | length }} Rooms, only those created by you are linked above.
|
||||||
|
</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -14,10 +14,15 @@
|
||||||
<div id="weighted-settings" data-game="{{ game }}">
|
<div id="weighted-settings" data-game="{{ game }}">
|
||||||
<div id="user-message"></div>
|
<div id="user-message"></div>
|
||||||
<h1>Weighted Settings</h1>
|
<h1>Weighted Settings</h1>
|
||||||
|
<p>Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
|
||||||
|
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||||
|
entries in a raffle.</p>
|
||||||
|
|
||||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
<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>
|
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>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||||
|
page.</p>
|
||||||
|
|
||||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
<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 />
|
items if you are playing in a MultiWorld.</label><br />
|
||||||
|
|
|
@ -421,7 +421,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
||||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png",
|
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
|
||||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||||
|
@ -429,7 +429,6 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
||||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||||
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
minecraft_location_ids = {
|
minecraft_location_ids = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Dict, Set, Tuple, List, Optional, TextIO
|
from typing import Dict, Set, Tuple, List, Optional, TextIO, Any
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, Item, CollectionState, Location
|
from BaseClasses import MultiWorld, Item, CollectionState, Location
|
||||||
from Options import Option
|
from Options import Option
|
||||||
|
@ -8,7 +8,7 @@ from Options import Option
|
||||||
class AutoWorldRegister(type):
|
class AutoWorldRegister(type):
|
||||||
world_types: Dict[str, World] = {}
|
world_types: Dict[str, World] = {}
|
||||||
|
|
||||||
def __new__(cls, name, bases, dct):
|
def __new__(cls, name: str, bases, dct: Dict[str, Any]):
|
||||||
# filter out any events
|
# filter out any events
|
||||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||||
|
@ -19,7 +19,7 @@ class AutoWorldRegister(type):
|
||||||
# build rest
|
# build rest
|
||||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||||
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
|
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||||
|
|
||||||
# construct class
|
# construct class
|
||||||
new_class = super().__new__(cls, name, bases, dct)
|
new_class = super().__new__(cls, name, bases, dct)
|
||||||
|
@ -71,7 +71,7 @@ class World(metaclass=AutoWorldRegister):
|
||||||
options: Dict[str, type(Option)] = {} # link your Options mapping
|
options: Dict[str, type(Option)] = {} # link your Options mapping
|
||||||
game: str # name the game
|
game: str # name the game
|
||||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||||
all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names
|
all_item_and_group_names: Set[str] = frozenset() # gets automatically populated with all item and item group names
|
||||||
|
|
||||||
# map names to their IDs
|
# map names to their IDs
|
||||||
item_name_to_id: Dict[str, int] = {}
|
item_name_to_id: Dict[str, int] = {}
|
||||||
|
@ -115,7 +115,8 @@ class World(metaclass=AutoWorldRegister):
|
||||||
item_names: Set[str] # set of all potential item names
|
item_names: Set[str] # set of all potential item names
|
||||||
location_names: Set[str] # set of all potential location names
|
location_names: Set[str] # set of all potential location names
|
||||||
|
|
||||||
# If there is visibility in what is being sent, this is where it will be known.
|
# If the game displays all contained items to the user, this flag pre-fills the hint system with this information
|
||||||
|
# For example the "full" tech tree information option in Factorio
|
||||||
sending_visible: bool = False
|
sending_visible: bool = False
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import typing
|
import typing
|
||||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList, DeathLink
|
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink
|
||||||
|
from .LogicTricks import normalized_name_tricks
|
||||||
from .ColorSFXOptions import *
|
from .ColorSFXOptions import *
|
||||||
|
|
||||||
|
|
||||||
|
@ -824,6 +825,8 @@ class LogicTricks(OptionList):
|
||||||
Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"].
|
Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"].
|
||||||
A full list of supported tricks can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py"""
|
A full list of supported tricks can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py"""
|
||||||
displayname = "Logic Tricks"
|
displayname = "Logic Tricks"
|
||||||
|
valid_keys = frozenset(normalized_name_tricks)
|
||||||
|
valid_keys_casefold = True
|
||||||
|
|
||||||
|
|
||||||
# All options assembled into a single dict
|
# All options assembled into a single dict
|
||||||
|
|
|
@ -143,7 +143,7 @@ def set_rules(ootworld):
|
||||||
if ootworld.skip_child_zelda:
|
if ootworld.skip_child_zelda:
|
||||||
# If skip child zelda is on, the item at Song from Impa must be giveable by the save context.
|
# 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)
|
location = world.get_location('Song from Impa', player)
|
||||||
add_item_rule(location, lambda item: item in SaveContext.giveable_items)
|
add_item_rule(location, lambda item: item.name 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)
|
||||||
|
|
|
@ -58,7 +58,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||||
|
|
||||||
names: Dict[str, int] = {}
|
names: Dict[str, int] = {}
|
||||||
|
|
||||||
connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player)))
|
connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player) or state.has('Talaria Attachment', player))
|
||||||
connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Serene', 'Region', player))
|
connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Serene', 'Region', player))
|
||||||
connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: state._timespinner_has_doublejump(world, player))
|
connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: state._timespinner_has_doublejump(world, player))
|
||||||
connect(world, player, names, 'Lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
connect(world, player, names, 'Lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||||
|
|
Loading…
Reference in New Issue