diff --git a/BaseClasses.py b/BaseClasses.py index ada18f1e..94eb10de 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1453,6 +1453,14 @@ class Tutorial(NamedTuple): authors: List[str] +class OptionGroup(NamedTuple): + """Define a grouping of options""" + name: str + """Name of the group to categorize this option in for display on the WebHost and in generated YAMLS.""" + options: List[Type[Options.Option]] + """Options to be in the defined group. """ + + class PlandoOptions(IntFlag): none = 0b0000 items = 0b0001 diff --git a/Options.py b/Options.py index 43800708..6b4db10a 100644 --- a/Options.py +++ b/Options.py @@ -24,7 +24,7 @@ if typing.TYPE_CHECKING: class OptionError(ValueError): pass - + class Visibility(enum.IntFlag): none = 0b0000 template = 0b0001 @@ -947,7 +947,7 @@ class CommonOptions(metaclass=OptionsMetaProperty): def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: """ Returns a dictionary of [str, Option.value] - + :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` """ @@ -1161,15 +1161,21 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - all_options: typing.Dict[str, AssembleOptions] = { - option_name: option for option_name, option in world.options_dataclass.type_hints.items() - if option.visibility & Visibility.template - } + + option_groups = {option: option_group.name + for option_group in world.web.option_groups + for option in option_group.options} + ordered_groups = ["Game Options"] + [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] + grouped_options = {group: {} for group in ordered_groups} + for option_name, option in world.options_dataclass.type_hints.items(): + if option.visibility >= Visibility.template: + grouped_options[option_groups.get(option, "Game Options")][option_name] = option with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( - options=all_options, + option_groups=grouped_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, dictify_range=dictify_range, ) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 7d2a32d3..fdf3037f 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -84,6 +84,6 @@ def register(): from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it - from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 018ad1bd..5072f113 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -37,25 +37,6 @@ def start_playing(): return render_template(f"startPlaying.html") -# TODO for back compat. remove around 0.4.5 -@app.route("/weighted-settings") -def weighted_settings(): - return redirect("weighted-options", 301) - - -@app.route("/weighted-options") -@cache.cached() -def weighted_options(): - return render_template("weighted-options.html") - - -# Player options pages -@app.route("/games//player-options") -@cache.cached() -def player_options(game: str): - return render_template("player-options.html", game=game, theme=get_world_theme(game)) - - # Game Info Pages @app.route('/games//info/') @cache.cached() diff --git a/WebHostLib/options.py b/WebHostLib/options.py index b3fd8d61..e631d31b 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,14 +1,17 @@ -import json -import logging +import collections.abc import os -import typing +import yaml +import requests +import json +import flask import Options -from Utils import local_path +from Options import Visibility +from flask import redirect, render_template, request, Response from worlds.AutoWorld import AutoWorldRegister - -handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", - "exclude_locations", "priority_locations"} +from Utils import local_path +from textwrap import dedent +from . import app, cache def create(): @@ -17,189 +20,230 @@ def create(): Options.generate_yaml_templates(yaml_folder) - def get_html_doc(option_type: type(Options.Option)) -> str: - if not option_type.__doc__: - return "Please document me!" - return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip() - weighted_options = { - "baseOptions": { - "description": "Generated by https://archipelago.gg/", - "name": "", - "game": {}, +def get_world_theme(game_name: str): + if game_name in AutoWorldRegister.world_types: + return AutoWorldRegister.world_types[game_name].web.theme + return 'grass' + + +def render_options_page(template: str, world_name: str, is_complex: bool = False): + world = AutoWorldRegister.world_types[world_name] + if world.hidden or world.web.options_page is False: + return redirect("games") + + option_groups = {option: option_group.name + for option_group in world.web.option_groups + for option in option_group.options} + ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]] + grouped_options = {group: {} for group in ordered_groups} + for option_name, option in world.options_dataclass.type_hints.items(): + # Exclude settings from options pages if their visibility is disabled + if not is_complex and option.visibility < Visibility.simple_ui: + continue + + if is_complex and option.visibility < Visibility.complex_ui: + continue + + grouped_options[option_groups.get(option, "Game Options")][option_name] = option + + return render_template( + template, + world_name=world_name, + world=world, + option_groups=grouped_options, + issubclass=issubclass, + Options=Options, + theme=get_world_theme(world_name), + ) + + +def generate_game(player_name: str, formatted_options: dict): + payload = { + "race": 0, + "hint_cost": 10, + "forfeit_mode": "auto", + "remaining_mode": "disabled", + "collect_mode": "goal", + "weights": { + player_name: formatted_options, }, - "games": {}, } + r = requests.post("https://archipelago.gg/api/generate", json=payload) + if 200 <= r.status_code <= 299: + response_data = r.json() + return redirect(response_data["url"]) + else: + return r.text - for game_name, world in AutoWorldRegister.world_types.items(): - all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints +def send_yaml(player_name: str, formatted_options: dict): + response = Response(yaml.dump(formatted_options, sort_keys=False)) + response.headers["Content-Type"] = "text/yaml" + response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml" + return response - # Generate JSON files for player-options pages - player_options = { - "baseOptions": { - "description": f"Generated by https://archipelago.gg/ for {game_name}", - "game": game_name, - "name": "", - }, - } - game_options = {} - visible: typing.Set[str] = set() - visible_weighted: typing.Set[str] = set() +@app.template_filter("dedent") +def filter_dedent(text: str): + return dedent(text).strip("\n ") - for option_name, option in all_options.items(): - if option.visibility & Options.Visibility.simple_ui: - visible.add(option_name) - if option.visibility & Options.Visibility.complex_ui: - visible_weighted.add(option_name) - if option_name in handled_in_js: - pass +@app.template_test("ordered") +def test_ordered(obj): + return isinstance(obj, collections.abc.Sequence) - elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle): - game_options[option_name] = this_option = { - "type": "select", - "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": get_html_doc(option), - "defaultValue": None, - "options": [] - } - for sub_option_id, sub_option_name in option.name_lookup.items(): - if sub_option_name != "random": - this_option["options"].append({ - "name": option.get_option_name(sub_option_id), - "value": sub_option_name, - }) - if sub_option_id == option.default: - this_option["defaultValue"] = sub_option_name +@app.route("/games//option-presets", methods=["GET"]) +@cache.cached() +def option_presets(game: str) -> Response: + world = AutoWorldRegister.world_types[game] + presets = {} - if not this_option["defaultValue"]: - this_option["defaultValue"] = "random" + if world.web.options_presets: + presets = presets | world.web.options_presets - elif issubclass(option, Options.Range): - game_options[option_name] = { - "type": "range", - "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": get_html_doc(option), - "defaultValue": option.default if hasattr( - option, "default") and option.default != "random" else option.range_start, - "min": option.range_start, - "max": option.range_end, - } + class SetEncoder(json.JSONEncoder): + def default(self, obj): + from collections.abc import Set + if isinstance(obj, Set): + return list(obj) + return json.JSONEncoder.default(self, obj) - if issubclass(option, Options.NamedRange): - game_options[option_name]["type"] = 'named_range' - game_options[option_name]["value_names"] = {} - for key, val in option.special_range_names.items(): - game_options[option_name]["value_names"][key] = val + json_data = json.dumps(presets, cls=SetEncoder) + response = flask.Response(json_data) + response.headers["Content-Type"] = "application/json" + return response - elif issubclass(option, Options.ItemSet): - game_options[option_name] = { - "type": "items-list", - "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": get_html_doc(option), - "defaultValue": list(option.default) - } - elif issubclass(option, Options.LocationSet): - game_options[option_name] = { - "type": "locations-list", - "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": get_html_doc(option), - "defaultValue": list(option.default) - } +@app.route("/weighted-options") +def weighted_options_old(): + return redirect("games", 301) - elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict): - if option.valid_keys: - game_options[option_name] = { - "type": "custom-list", - "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": get_html_doc(option), - "options": list(option.valid_keys), - "defaultValue": list(option.default) if hasattr(option, "default") else [] - } - else: - logging.debug(f"{option} not exported to Web Options.") +@app.route("/games//weighted-options") +@cache.cached() +def weighted_options(game: str): + return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True) - player_options["presetOptions"] = {} - for preset_name, preset in world.web.options_presets.items(): - player_options["presetOptions"][preset_name] = {} - for option_name, option_value in preset.items(): - # Random range type settings are not valid. - assert (not str(option_value).startswith("random-")), \ - f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \ - f"values are not supported for presets." - # Normal random is supported, but needs to be handled explicitly. - if option_value == "random": - player_options["presetOptions"][preset_name][option_name] = option_value +@app.route("/games//generate-weighted-yaml", methods=["POST"]) +def generate_weighted_yaml(game: str): + if request.method == "POST": + intent_generate = False + options = {} + + for key, val in request.form.items(): + if "||" not in key: + if len(str(val)) == 0: continue - option = world.options_dataclass.type_hints[option_name].from_any(option_value) - if isinstance(option, Options.NamedRange) and isinstance(option_value, str): - assert option_value in option.special_range_names, \ - f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \ - f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." + options[key] = val + else: + if int(val) == 0: + continue - # Still use the true value for the option, not the name. - player_options["presetOptions"][preset_name][option_name] = option.value - elif isinstance(option, Options.Range): - player_options["presetOptions"][preset_name][option_name] = option.value - elif isinstance(option_value, str): - # For Choice and Toggle options, the value should be the name of the option. This is to prevent - # setting a preset for an option with an overridden from_text method that would normally be okay, - # but would not be okay for the webhost's current implementation of player options UI. - assert option.name_lookup[option.value] == option_value, \ - f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \ - f"Values must not be resolved to a different option via option.from_text (or an alias)." - player_options["presetOptions"][preset_name][option_name] = option.current_key - else: - # int and bool values are fine, just resolve them to the current key for webhost. - player_options["presetOptions"][preset_name][option_name] = option.current_key + [option, setting] = key.split("||") + options.setdefault(option, {})[setting] = int(val) - os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) + # Error checking + if "name" not in options: + return "Player name is required." - filtered_player_options = player_options - filtered_player_options["gameOptions"] = { - option_name: option_data for option_name, option_data in game_options.items() - if option_name in visible + # Remove POST data irrelevant to YAML + if "intent-generate" in options: + intent_generate = True + del options["intent-generate"] + if "intent-export" in options: + del options["intent-export"] + + # Properly format YAML output + player_name = options["name"] + del options["name"] + + formatted_options = { + "name": player_name, + "game": game, + "description": f"Generated by https://archipelago.gg/ for {game}", + game: options, } - with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f: - json.dump(filtered_player_options, f, indent=2, separators=(',', ': ')) + if intent_generate: + return generate_game(player_name, formatted_options) - filtered_player_options["gameOptions"] = { - option_name: option_data for option_name, option_data in game_options.items() - if option_name in visible_weighted + else: + return send_yaml(player_name, formatted_options) + + +# Player options pages +@app.route("/games//player-options") +@cache.cached() +def player_options(game: str): + return render_options_page("playerOptions/playerOptions.html", game, is_complex=False) + + +# YAML generator for player-options +@app.route("/games//generate-yaml", methods=["POST"]) +def generate_yaml(game: str): + if request.method == "POST": + options = {} + intent_generate = False + for key, val in request.form.items(multi=True): + if key in options: + if not isinstance(options[key], list): + options[key] = [options[key]] + options[key].append(val) + else: + options[key] = val + + # Detect and build ItemDict options from their name pattern + for key, val in options.copy().items(): + key_parts = key.rsplit("||", 2) + if key_parts[-1] == "qty": + if key_parts[0] not in options: + options[key_parts[0]] = {} + if val != "0": + options[key_parts[0]][key_parts[1]] = int(val) + del options[key] + + # Detect random-* keys and set their options accordingly + for key, val in options.copy().items(): + if key.startswith("random-"): + options[key.removeprefix("random-")] = "random" + del options[key] + + # Error checking + if not options["name"]: + return "Player name is required." + + # Remove POST data irrelevant to YAML + preset_name = 'default' + if "intent-generate" in options: + intent_generate = True + del options["intent-generate"] + if "intent-export" in options: + del options["intent-export"] + if "game-options-preset" in options: + preset_name = options["game-options-preset"] + del options["game-options-preset"] + + # Properly format YAML output + player_name = options["name"] + del options["name"] + + description = f"Generated by https://archipelago.gg/ for {game}" + if preset_name != 'default' and preset_name != 'custom': + description += f" using {preset_name} preset" + + formatted_options = { + "name": player_name, + "game": game, + "description": description, + game: options, } - if not world.hidden and world.web.options_page is True: - # Add the random option to Choice, TextChoice, and Toggle options - for option in filtered_player_options["gameOptions"].values(): - if option["type"] == "select": - option["options"].append({"name": "Random", "value": "random"}) - - if not option["defaultValue"]: - option["defaultValue"] = "random" - - weighted_options["baseOptions"]["game"][game_name] = 0 - weighted_options["games"][game_name] = { - "gameSettings": filtered_player_options["gameOptions"], - "gameItems": tuple(world.item_names), - "gameItemGroups": [ - group for group in world.item_name_groups.keys() if group != "Everything" - ], - "gameItemDescriptions": world.item_descriptions, - "gameLocations": tuple(world.location_names), - "gameLocationGroups": [ - group for group in world.location_name_groups.keys() if group != "Everywhere" - ], - "gameLocationDescriptions": world.location_descriptions, - } - - with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: - json.dump(weighted_options, f, indent=2, separators=(',', ': ')) + if intent_generate: + return generate_game(player_name, formatted_options) + else: + return send_yaml(player_name, formatted_options) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js deleted file mode 100644 index 92cd6c43..00000000 --- a/WebHostLib/static/assets/player-options.js +++ /dev/null @@ -1,523 +0,0 @@ -let gameName = null; - -window.addEventListener('load', () => { - gameName = document.getElementById('player-options').getAttribute('data-game'); - - // Update game name on page - document.getElementById('game-name').innerText = gameName; - - fetchOptionData().then((results) => { - let optionHash = localStorage.getItem(`${gameName}-hash`); - if (!optionHash) { - // If no hash data has been set before, set it now - optionHash = md5(JSON.stringify(results)); - localStorage.setItem(`${gameName}-hash`, optionHash); - localStorage.removeItem(gameName); - } - - if (optionHash !== md5(JSON.stringify(results))) { - showUserMessage( - 'Your options are out of date! Click here to update them! Be aware this will reset them all to default.' - ); - document.getElementById('user-message').addEventListener('click', resetOptions); - } - - // Page setup - createDefaultOptions(results); - buildUI(results); - adjustHeaderWidth(); - - // Event listeners - document.getElementById('export-options').addEventListener('click', () => exportOptions()); - document.getElementById('generate-race').addEventListener('click', () => generateGame(true)); - document.getElementById('generate-game').addEventListener('click', () => generateGame()); - - // Name input field - const playerOptions = JSON.parse(localStorage.getItem(gameName)); - const nameInput = document.getElementById('player-name'); - nameInput.addEventListener('keyup', (event) => updateBaseOption(event)); - nameInput.value = playerOptions.name; - - // Presets - const presetSelect = document.getElementById('game-options-preset'); - presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value)); - for (const preset in results['presetOptions']) { - const presetOption = document.createElement('option'); - presetOption.innerText = preset; - presetSelect.appendChild(presetOption); - } - presetSelect.value = localStorage.getItem(`${gameName}-preset`); - results['presetOptions']['__default'] = {}; - }).catch((e) => { - console.error(e); - const url = new URL(window.location.href); - window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`); - }) -}); - -const resetOptions = () => { - localStorage.removeItem(gameName); - localStorage.removeItem(`${gameName}-hash`); - localStorage.removeItem(`${gameName}-preset`); - window.location.reload(); -}; - -const fetchOptionData = () => new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status !== 200) { - reject(ajax.responseText); - return; - } - try{ resolve(JSON.parse(ajax.responseText)); } - catch(error){ reject(error); } - }; - ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true); - ajax.send(); -}); - -const createDefaultOptions = (optionData) => { - if (!localStorage.getItem(gameName)) { - const newOptions = { - [gameName]: {}, - }; - for (let baseOption of Object.keys(optionData.baseOptions)){ - newOptions[baseOption] = optionData.baseOptions[baseOption]; - } - for (let gameOption of Object.keys(optionData.gameOptions)){ - newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue; - } - localStorage.setItem(gameName, JSON.stringify(newOptions)); - } - - if (!localStorage.getItem(`${gameName}-preset`)) { - localStorage.setItem(`${gameName}-preset`, '__default'); - } -}; - -const buildUI = (optionData) => { - // Game Options - const leftGameOpts = {}; - const rightGameOpts = {}; - Object.keys(optionData.gameOptions).forEach((key, index) => { - if (index < Object.keys(optionData.gameOptions).length / 2) { - leftGameOpts[key] = optionData.gameOptions[key]; - } else { - rightGameOpts[key] = optionData.gameOptions[key]; - } - }); - document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts)); - document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts)); -}; - -const buildOptionsTable = (options, romOpts = false) => { - const currentOptions = JSON.parse(localStorage.getItem(gameName)); - const table = document.createElement('table'); - const tbody = document.createElement('tbody'); - - Object.keys(options).forEach((option) => { - const tr = document.createElement('tr'); - - // td Left - const tdl = document.createElement('td'); - const label = document.createElement('label'); - label.textContent = `${options[option].displayName}: `; - label.setAttribute('for', option); - - const questionSpan = document.createElement('span'); - questionSpan.classList.add('interactive'); - questionSpan.setAttribute('data-tooltip', options[option].description); - questionSpan.innerText = '(?)'; - - label.appendChild(questionSpan); - tdl.appendChild(label); - tr.appendChild(tdl); - - // td Right - const tdr = document.createElement('td'); - let element = null; - - const randomButton = document.createElement('button'); - - switch(options[option].type) { - case 'select': - element = document.createElement('div'); - element.classList.add('select-container'); - let select = document.createElement('select'); - select.setAttribute('id', option); - select.setAttribute('data-key', option); - if (romOpts) { select.setAttribute('data-romOpt', '1'); } - options[option].options.forEach((opt) => { - const optionElement = document.createElement('option'); - optionElement.setAttribute('value', opt.value); - optionElement.innerText = opt.name; - - if ((isNaN(currentOptions[gameName][option]) && - (parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) || - (opt.value === currentOptions[gameName][option])) - { - optionElement.selected = true; - } - select.appendChild(optionElement); - }); - select.addEventListener('change', (event) => updateGameOption(event.target)); - element.appendChild(select); - - // Randomize button - randomButton.innerText = '🎲'; - randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', option); - randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); - randomButton.addEventListener('click', (event) => toggleRandomize(event, select)); - if (currentOptions[gameName][option] === 'random') { - randomButton.classList.add('active'); - select.disabled = true; - } - - element.appendChild(randomButton); - break; - - case 'range': - element = document.createElement('div'); - element.classList.add('range-container'); - - let range = document.createElement('input'); - range.setAttribute('id', option); - range.setAttribute('type', 'range'); - range.setAttribute('data-key', option); - range.setAttribute('min', options[option].min); - range.setAttribute('max', options[option].max); - range.value = currentOptions[gameName][option]; - range.addEventListener('change', (event) => { - document.getElementById(`${option}-value`).innerText = event.target.value; - updateGameOption(event.target); - }); - element.appendChild(range); - - let rangeVal = document.createElement('span'); - rangeVal.classList.add('range-value'); - rangeVal.setAttribute('id', `${option}-value`); - rangeVal.innerText = currentOptions[gameName][option] !== 'random' ? - currentOptions[gameName][option] : options[option].defaultValue; - element.appendChild(rangeVal); - - // Randomize button - randomButton.innerText = '🎲'; - randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', option); - randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); - randomButton.addEventListener('click', (event) => toggleRandomize(event, range)); - if (currentOptions[gameName][option] === 'random') { - randomButton.classList.add('active'); - range.disabled = true; - } - - element.appendChild(randomButton); - break; - - case 'named_range': - element = document.createElement('div'); - element.classList.add('named-range-container'); - - // Build the select element - let namedRangeSelect = document.createElement('select'); - namedRangeSelect.setAttribute('data-key', option); - Object.keys(options[option].value_names).forEach((presetName) => { - let presetOption = document.createElement('option'); - presetOption.innerText = presetName; - presetOption.value = options[option].value_names[presetName]; - const words = presetOption.innerText.split('_'); - for (let i = 0; i < words.length; i++) { - words[i] = words[i][0].toUpperCase() + words[i].substring(1); - } - presetOption.innerText = words.join(' '); - namedRangeSelect.appendChild(presetOption); - }); - let customOption = document.createElement('option'); - customOption.innerText = 'Custom'; - customOption.value = 'custom'; - customOption.selected = true; - namedRangeSelect.appendChild(customOption); - if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) { - namedRangeSelect.value = Number(currentOptions[gameName][option]); - } - - // Build range element - let namedRangeWrapper = document.createElement('div'); - namedRangeWrapper.classList.add('named-range-wrapper'); - let namedRange = document.createElement('input'); - namedRange.setAttribute('type', 'range'); - namedRange.setAttribute('data-key', option); - namedRange.setAttribute('min', options[option].min); - namedRange.setAttribute('max', options[option].max); - namedRange.value = currentOptions[gameName][option]; - - // Build rage value element - let namedRangeVal = document.createElement('span'); - namedRangeVal.classList.add('range-value'); - namedRangeVal.setAttribute('id', `${option}-value`); - namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? - currentOptions[gameName][option] : options[option].defaultValue; - - // Configure select event listener - namedRangeSelect.addEventListener('change', (event) => { - if (event.target.value === 'custom') { return; } - - // Update range slider - namedRange.value = event.target.value; - document.getElementById(`${option}-value`).innerText = event.target.value; - updateGameOption(event.target); - }); - - // Configure range event handler - namedRange.addEventListener('change', (event) => { - // Update select element - namedRangeSelect.value = - (Object.values(options[option].value_names).includes(parseInt(event.target.value))) ? - parseInt(event.target.value) : 'custom'; - document.getElementById(`${option}-value`).innerText = event.target.value; - updateGameOption(event.target); - }); - - element.appendChild(namedRangeSelect); - namedRangeWrapper.appendChild(namedRange); - namedRangeWrapper.appendChild(namedRangeVal); - element.appendChild(namedRangeWrapper); - - // Randomize button - randomButton.innerText = '🎲'; - randomButton.classList.add('randomize-button'); - randomButton.setAttribute('data-key', option); - randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); - randomButton.addEventListener('click', (event) => toggleRandomize( - event, namedRange, namedRangeSelect) - ); - if (currentOptions[gameName][option] === 'random') { - randomButton.classList.add('active'); - namedRange.disabled = true; - namedRangeSelect.disabled = true; - } - - namedRangeWrapper.appendChild(randomButton); - break; - - default: - console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`); - return; - } - - tdr.appendChild(element); - tr.appendChild(tdr); - tbody.appendChild(tr); - }); - - table.appendChild(tbody); - return table; -}; - -const setPresets = (optionsData, presetName) => { - const defaults = optionsData['gameOptions']; - const preset = optionsData['presetOptions'][presetName]; - - localStorage.setItem(`${gameName}-preset`, presetName); - - if (!preset) { - console.error(`No presets defined for preset name: '${presetName}'`); - return; - } - - const updateOptionElement = (option, presetValue) => { - const optionElement = document.querySelector(`#${option}[data-key='${option}']`); - const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); - - if (presetValue === 'random') { - randomElement.classList.add('active'); - optionElement.disabled = true; - updateGameOption(randomElement, false); - } else { - optionElement.value = presetValue; - randomElement.classList.remove('active'); - optionElement.disabled = undefined; - updateGameOption(optionElement, false); - } - }; - - for (const option in defaults) { - let presetValue = preset[option]; - if (presetValue === undefined) { - // Using the default value if not set in presets. - presetValue = defaults[option]['defaultValue']; - } - - switch (defaults[option].type) { - case 'range': - const numberElement = document.querySelector(`#${option}-value`); - if (presetValue === 'random') { - numberElement.innerText = defaults[option]['defaultValue'] === 'random' - ? defaults[option]['min'] // A fallback so we don't print 'random' in the UI. - : defaults[option]['defaultValue']; - } else { - numberElement.innerText = presetValue; - } - - updateOptionElement(option, presetValue); - break; - - case 'select': { - updateOptionElement(option, presetValue); - break; - } - - case 'named_range': { - const selectElement = document.querySelector(`select[data-key='${option}']`); - const rangeElement = document.querySelector(`input[data-key='${option}']`); - const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); - - if (presetValue === 'random') { - randomElement.classList.add('active'); - selectElement.disabled = true; - rangeElement.disabled = true; - updateGameOption(randomElement, false); - } else { - rangeElement.value = presetValue; - selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ? - parseInt(presetValue) : 'custom'; - document.getElementById(`${option}-value`).innerText = presetValue; - - randomElement.classList.remove('active'); - selectElement.disabled = undefined; - rangeElement.disabled = undefined; - updateGameOption(rangeElement, false); - } - break; - } - - default: - console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`); - break; - } - } -}; - -const toggleRandomize = (event, inputElement, optionalSelectElement = null) => { - const active = event.target.classList.contains('active'); - const randomButton = event.target; - - if (active) { - randomButton.classList.remove('active'); - inputElement.disabled = undefined; - if (optionalSelectElement) { - optionalSelectElement.disabled = undefined; - } - } else { - randomButton.classList.add('active'); - inputElement.disabled = true; - if (optionalSelectElement) { - optionalSelectElement.disabled = true; - } - } - updateGameOption(active ? inputElement : randomButton); -}; - -const updateBaseOption = (event) => { - const options = JSON.parse(localStorage.getItem(gameName)); - options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ? - event.target.value : parseInt(event.target.value); - localStorage.setItem(gameName, JSON.stringify(options)); -}; - -const updateGameOption = (optionElement, toggleCustomPreset = true) => { - const options = JSON.parse(localStorage.getItem(gameName)); - - if (toggleCustomPreset) { - localStorage.setItem(`${gameName}-preset`, '__custom'); - const presetElement = document.getElementById('game-options-preset'); - presetElement.value = '__custom'; - } - - if (optionElement.classList.contains('randomize-button')) { - // If the event passed in is the randomize button, then we know what we must do. - options[gameName][optionElement.getAttribute('data-key')] = 'random'; - } else { - options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ? - optionElement.value : parseInt(optionElement.value, 10); - } - - localStorage.setItem(gameName, JSON.stringify(options)); -}; - -const exportOptions = () => { - const options = JSON.parse(localStorage.getItem(gameName)); - const preset = localStorage.getItem(`${gameName}-preset`); - switch (preset) { - case '__default': - options['description'] = `Generated by https://archipelago.gg with the default preset.`; - break; - - case '__custom': - options['description'] = `Generated by https://archipelago.gg.`; - break; - - default: - options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`; - } - - if (!options.name || options.name.toString().trim().length === 0) { - return showUserMessage('You must enter a player name!'); - } - const yamlText = jsyaml.safeDump(options, { 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) => { - const options = JSON.parse(localStorage.getItem(gameName)); - if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) { - return showUserMessage('You must enter a player name!'); - } - - axios.post('/api/generate', { - weights: { player: options }, - presetData: { player: options }, - playerCount: 1, - spoiler: 3, - race: raceMode ? '1' : '0', - }).then((response) => { - window.location.href = response.data.url; - }).catch((error) => { - let userMessage = 'Something went wrong and your game could not be generated.'; - if (error.response.data.text) { - userMessage += ' ' + error.response.data.text; - } - showUserMessage(userMessage); - console.error(error); - }); -}; - -const showUserMessage = (message) => { - const userMessage = document.getElementById('user-message'); - userMessage.innerText = message; - userMessage.classList.add('visible'); - window.scrollTo(0, 0); - userMessage.addEventListener('click', () => { - userMessage.classList.remove('visible'); - userMessage.addEventListener('click', hideUserMessage); - }); -}; - -const hideUserMessage = () => { - const userMessage = document.getElementById('user-message'); - userMessage.classList.remove('visible'); - userMessage.removeEventListener('click', hideUserMessage); -}; diff --git a/WebHostLib/static/assets/playerOptions.js b/WebHostLib/static/assets/playerOptions.js new file mode 100644 index 00000000..d0f2e388 --- /dev/null +++ b/WebHostLib/static/assets/playerOptions.js @@ -0,0 +1,335 @@ +let presets = {}; + +window.addEventListener('load', async () => { + // Load settings from localStorage, if available + loadSettings(); + + // Fetch presets if available + await fetchPresets(); + + // Handle changes to range inputs + document.querySelectorAll('input[type=range]').forEach((range) => { + const optionName = range.getAttribute('id'); + range.addEventListener('change', () => { + document.getElementById(`${optionName}-value`).innerText = range.value; + + // Handle updating named range selects to "custom" if appropriate + const select = document.querySelector(`select[data-option-name=${optionName}]`); + if (select) { + let updated = false; + select?.childNodes.forEach((option) => { + if (option.value === range.value) { + select.value = range.value; + updated = true; + } + }); + if (!updated) { + select.value = 'custom'; + } + } + }); + }); + + // Handle changes to named range selects + document.querySelectorAll('.named-range-container select').forEach((select) => { + const optionName = select.getAttribute('data-option-name'); + select.addEventListener('change', (evt) => { + document.getElementById(optionName).value = evt.target.value; + document.getElementById(`${optionName}-value`).innerText = evt.target.value; + }); + }); + + // Handle changes to randomize checkboxes + document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => { + const optionName = checkbox.getAttribute('data-option-name'); + checkbox.addEventListener('change', () => { + const optionInput = document.getElementById(optionName); + const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`); + const customInput = document.getElementById(`${optionName}-custom`); + if (checkbox.checked) { + optionInput.setAttribute('disabled', '1'); + namedRangeSelect?.setAttribute('disabled', '1'); + if (customInput) { + customInput.setAttribute('disabled', '1'); + } + } else { + optionInput.removeAttribute('disabled'); + namedRangeSelect?.removeAttribute('disabled'); + if (customInput) { + customInput.removeAttribute('disabled'); + } + } + }); + }); + + // Handle changes to TextChoice input[type=text] + document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => { + const optionName = input.getAttribute('data-option-name'); + input.addEventListener('input', () => { + const select = document.getElementById(optionName); + const optionValues = []; + select.childNodes.forEach((option) => optionValues.push(option.value)); + select.value = (optionValues.includes(input.value)) ? input.value : 'custom'; + }); + }); + + // Handle changes to TextChoice select + document.querySelectorAll('.text-choice-container select').forEach((select) => { + const optionName = select.getAttribute('id'); + select.addEventListener('change', () => { + document.getElementById(`${optionName}-custom`).value = ''; + }); + }); + + // Update the "Option Preset" select to read "custom" when changes are made to relevant inputs + const presetSelect = document.getElementById('game-options-preset'); + document.querySelectorAll('input, select').forEach((input) => { + if ( // Ignore inputs which have no effect on yaml generation + (input.id === 'player-name') || + (input.id === 'game-options-preset') || + (input.classList.contains('group-toggle')) || + (input.type === 'submit') + ) { + return; + } + input.addEventListener('change', () => { + presetSelect.value = 'custom'; + }); + }); + + // Handle changes to presets select + document.getElementById('game-options-preset').addEventListener('change', choosePreset); + + // Save settings to localStorage when form is submitted + document.getElementById('options-form').addEventListener('submit', (evt) => { + const playerName = document.getElementById('player-name'); + if (!playerName.value.trim()) { + evt.preventDefault(); + window.scrollTo(0, 0); + showUserMessage('You must enter a player name!'); + } + + saveSettings(); + }); +}); + +// Save all settings to localStorage +const saveSettings = () => { + const options = { + inputs: {}, + checkboxes: {}, + }; + document.querySelectorAll('input, select').forEach((input) => { + if (input.type === 'submit') { + // Ignore submit inputs + } + else if (input.type === 'checkbox') { + options.checkboxes[input.id] = input.checked; + } + else { + options.inputs[input.id] = input.value + } + }); + const game = document.getElementById('player-options').getAttribute('data-game'); + localStorage.setItem(game, JSON.stringify(options)); +}; + +// Load all options from localStorage +const loadSettings = () => { + const game = document.getElementById('player-options').getAttribute('data-game'); + + const options = JSON.parse(localStorage.getItem(game)); + if (options) { + if (!options.inputs || !options.checkboxes) { + localStorage.removeItem(game); + return; + } + + // Restore value-based inputs and selects + Object.keys(options.inputs).forEach((key) => { + try{ + document.getElementById(key).value = options.inputs[key]; + const rangeValue = document.getElementById(`${key}-value`); + if (rangeValue) { + rangeValue.innerText = options.inputs[key]; + } + } catch (err) { + console.error(`Unable to restore value to input with id ${key}`); + } + }); + + // Restore checkboxes + Object.keys(options.checkboxes).forEach((key) => { + try{ + if (options.checkboxes[key]) { + document.getElementById(key).setAttribute('checked', '1'); + } + } catch (err) { + console.error(`Unable to restore value to input with id ${key}`); + } + }); + } + + // Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled + document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => { + const optionName = checkbox.getAttribute('data-option-name'); + if (checkbox.checked) { + const input = document.getElementById(optionName); + if (input) { + input.setAttribute('disabled', '1'); + } + const customInput = document.getElementById(`${optionName}-custom`); + if (customInput) { + customInput.setAttribute('disabled', '1'); + } + } + }); +}; + +/** + * Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen + * @returns {Promise} + */ +const fetchPresets = async () => { + const response = await fetch('option-presets'); + presets = await response.json(); + const presetSelect = document.getElementById('game-options-preset'); + presetSelect.removeAttribute('disabled'); + + const game = document.getElementById('player-options').getAttribute('data-game'); + const presetToApply = localStorage.getItem(`${game}-preset`); + const playerName = localStorage.getItem(`${game}-player`); + if (presetToApply) { + localStorage.removeItem(`${game}-preset`); + presetSelect.value = presetToApply; + applyPresets(presetToApply); + } + + if (playerName) { + document.getElementById('player-name').value = playerName; + localStorage.removeItem(`${game}-player`); + } +}; + +/** + * Clear the localStorage for this game and set a preset to be loaded upon page reload + * @param evt + */ +const choosePreset = (evt) => { + if (evt.target.value === 'custom') { return; } + + const game = document.getElementById('player-options').getAttribute('data-game'); + localStorage.removeItem(game); + + localStorage.setItem(`${game}-player`, document.getElementById('player-name').value); + if (evt.target.value !== 'default') { + localStorage.setItem(`${game}-preset`, evt.target.value); + } + + document.querySelectorAll('#options-form input, #options-form select').forEach((input) => { + if (input.id === 'player-name') { return; } + input.removeAttribute('value'); + }); + + window.location.replace(window.location.href); +}; + +const applyPresets = (presetName) => { + // Ignore the "default" preset, because it gets set automatically by Jinja + if (presetName === 'default') { + saveSettings(); + return; + } + + if (!presets[presetName]) { + console.error(`Unknown preset ${presetName} chosen`); + return; + } + + const preset = presets[presetName]; + Object.keys(preset).forEach((optionName) => { + const optionValue = preset[optionName]; + + // Handle List and Set options + if (Array.isArray(optionValue)) { + document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => { + if (optionValue.includes(checkbox.value)) { + checkbox.setAttribute('checked', '1'); + } else { + checkbox.removeAttribute('checked'); + } + }); + return; + } + + // Handle Dict options + if (typeof(optionValue) === 'object' && optionValue !== null) { + const itemNames = Object.keys(optionValue); + document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => { + const itemName = input.getAttribute('data-item-name'); + input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0 + }); + return; + } + + // Identify all possible elements + const normalInput = document.getElementById(optionName); + const customInput = document.getElementById(`${optionName}-custom`); + const rangeValue = document.getElementById(`${optionName}-value`); + const randomizeInput = document.getElementById(`random-${optionName}`); + const namedRangeSelect = document.getElementById(`${optionName}-select`); + + // It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here + let trueValue = optionValue; + if (namedRangeSelect) { + namedRangeSelect.querySelectorAll('option').forEach((opt) => { + if (opt.innerText.startsWith(optionValue)) { + trueValue = opt.value; + } + }); + namedRangeSelect.value = trueValue; + } + + // Handle options whose presets are "random" + if (optionValue === 'random') { + normalInput.setAttribute('disabled', '1'); + randomizeInput.setAttribute('checked', '1'); + if (customInput) { + customInput.setAttribute('disabled', '1'); + } + if (rangeValue) { + rangeValue.innerText = normalInput.value; + } + if (namedRangeSelect) { + namedRangeSelect.setAttribute('disabled', '1'); + } + return; + } + + // Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only) + normalInput.value = trueValue; + normalInput.removeAttribute('disabled'); + randomizeInput.removeAttribute('checked'); + if (customInput) { + document.getElementById(`${optionName}-custom`).removeAttribute('disabled'); + } + if (rangeValue) { + rangeValue.innerText = trueValue; + } + }); + + saveSettings(); +}; + +const showUserMessage = (text) => { + const userMessage = document.getElementById('user-message'); + userMessage.innerText = text; + userMessage.addEventListener('click', hideUserMessage); + userMessage.style.display = 'block'; +}; + +const hideUserMessage = () => { + const userMessage = document.getElementById('user-message'); + userMessage.removeEventListener('click', hideUserMessage); + userMessage.style.display = 'none'; +}; diff --git a/WebHostLib/static/assets/supportedGames.js b/WebHostLib/static/assets/supportedGames.js index 56eb15b5..b692db92 100644 --- a/WebHostLib/static/assets/supportedGames.js +++ b/WebHostLib/static/assets/supportedGames.js @@ -1,18 +1,16 @@ window.addEventListener('load', () => { // Add toggle listener to all elements with .collapse-toggle - const toggleButtons = document.querySelectorAll('.collapse-toggle'); - toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse)); + const toggleButtons = document.querySelectorAll('details'); // Handle game filter input const gameSearch = document.getElementById('game-search'); gameSearch.value = ''; gameSearch.addEventListener('input', (evt) => { if (!evt.target.value.trim()) { - // If input is empty, display all collapsed games + // If input is empty, display all games as collapsed return toggleButtons.forEach((header) => { header.style.display = null; - header.firstElementChild.innerText = '▶'; - header.nextElementSibling.classList.add('collapsed'); + header.removeAttribute('open'); }); } @@ -21,12 +19,10 @@ window.addEventListener('load', () => { // If the game name includes the search string, display the game. If not, hide it if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) { header.style.display = null; - header.firstElementChild.innerText = '▼'; - header.nextElementSibling.classList.remove('collapsed'); + header.setAttribute('open', '1'); } else { header.style.display = 'none'; - header.firstElementChild.innerText = '▶'; - header.nextElementSibling.classList.add('collapsed'); + header.removeAttribute('open'); } }); }); @@ -35,30 +31,14 @@ window.addEventListener('load', () => { document.getElementById('collapse-all').addEventListener('click', collapseAll); }); -const toggleCollapse = (evt) => { - const gameArrow = evt.target.firstElementChild; - const gameInfo = evt.target.nextElementSibling; - if (gameInfo.classList.contains('collapsed')) { - gameArrow.innerText = '▼'; - gameInfo.classList.remove('collapsed'); - } else { - gameArrow.innerText = '▶'; - gameInfo.classList.add('collapsed'); - } -}; - const expandAll = () => { - document.querySelectorAll('.collapse-toggle').forEach((header) => { - if (header.style.display === 'none') { return; } - header.firstElementChild.innerText = '▼'; - header.nextElementSibling.classList.remove('collapsed'); + document.querySelectorAll('details').forEach((detail) => { + detail.setAttribute('open', '1'); }); }; const collapseAll = () => { - document.querySelectorAll('.collapse-toggle').forEach((header) => { - if (header.style.display === 'none') { return; } - header.firstElementChild.innerText = '▶'; - header.nextElementSibling.classList.add('collapsed'); + document.querySelectorAll('details').forEach((detail) => { + detail.removeAttribute('open'); }); }; diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js deleted file mode 100644 index 80f8efd1..00000000 --- a/WebHostLib/static/assets/weighted-options.js +++ /dev/null @@ -1,1190 +0,0 @@ -window.addEventListener('load', () => { - fetchSettingData().then((data) => { - let settingHash = localStorage.getItem('weighted-settings-hash'); - if (!settingHash) { - // If no hash data has been set before, set it now - settingHash = md5(JSON.stringify(data)); - localStorage.setItem('weighted-settings-hash', settingHash); - localStorage.removeItem('weighted-settings'); - } - - if (settingHash !== md5(JSON.stringify(data))) { - 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.classList.add('visible'); - userMessage.addEventListener('click', resetSettings); - } - - // Page setup - const settings = new WeightedSettings(data); - settings.buildUI(); - settings.updateVisibleGames(); - adjustHeaderWidth(); - - // Event listeners - document.getElementById('export-options').addEventListener('click', () => settings.export()); - document.getElementById('generate-race').addEventListener('click', () => settings.generateGame(true)); - document.getElementById('generate-game').addEventListener('click', () => settings.generateGame()); - - // Name input field - const nameInput = document.getElementById('player-name'); - nameInput.setAttribute('data-type', 'data'); - nameInput.setAttribute('data-setting', 'name'); - nameInput.addEventListener('keyup', (evt) => settings.updateBaseSetting(evt)); - nameInput.value = settings.current.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-options.json`)).then((response) => { - try{ response.json().then((jsonObj) => resolve(jsonObj)); } - catch(error){ reject(error); } - }); -}); - -/// The weighted settings across all games. -class WeightedSettings { - // The data from the server describing the types of settings available for - // each game, as a JSON-safe blob. - data; - - // The settings chosen by the user as they'd appear in the YAML file, stored - // to and retrieved from local storage. - current; - - // A record mapping game names to the associated GameSettings. - games; - - constructor(data) { - this.data = data; - this.current = JSON.parse(localStorage.getItem('weighted-settings')); - this.games = Object.keys(this.data.games).map((game) => new GameSettings(this, game)); - if (this.current) { return; } - - this.current = {}; - - // Transfer base options directly - for (let baseOption of Object.keys(this.data.baseOptions)){ - this.current[baseOption] = this.data.baseOptions[baseOption]; - } - - // Set options per game - for (let game of Object.keys(this.data.games)) { - // Initialize game object - this.current[game] = {}; - - // Transfer game settings - for (let gameSetting of Object.keys(this.data.games[game].gameSettings)){ - this.current[game][gameSetting] = {}; - - const setting = this.data.games[game].gameSettings[gameSetting]; - switch(setting.type){ - case 'select': - setting.options.forEach((option) => { - this.current[game][gameSetting][option.value] = - (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0; - }); - break; - case 'range': - case 'named_range': - this.current[game][gameSetting]['random'] = 0; - this.current[game][gameSetting]['random-low'] = 0; - this.current[game][gameSetting]['random-middle'] = 0; - this.current[game][gameSetting]['random-high'] = 0; - if (setting.hasOwnProperty('defaultValue')) { - this.current[game][gameSetting][setting.defaultValue] = 25; - } else { - this.current[game][gameSetting][setting.min] = 25; - } - break; - - case 'items-list': - case 'locations-list': - case 'custom-list': - this.current[game][gameSetting] = setting.defaultValue; - break; - - default: - console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`); - } - } - - this.current[game].start_inventory = {}; - this.current[game].exclude_locations = []; - this.current[game].priority_locations = []; - this.current[game].local_items = []; - this.current[game].non_local_items = []; - this.current[game].start_hints = []; - this.current[game].start_location_hints = []; - } - - this.save(); - } - - // Saves the current settings to local storage. - save() { - localStorage.setItem('weighted-settings', JSON.stringify(this.current)); - } - - buildUI() { - // Build the game-choice div - this.#buildGameChoice(); - - const gamesWrapper = document.getElementById('games-wrapper'); - this.games.forEach((game) => { - gamesWrapper.appendChild(game.buildUI()); - }); - } - - #buildGameChoice() { - 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(this.data.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 = this.current.game[game]; - range.addEventListener('change', (evt) => { - this.updateBaseSetting(evt); - this.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); - } - - // Verifies that `this.settings` meets all the requirements for world - // generation, normalizes it for serialization, and returns the result. - #validateSettings() { - const settings = structuredClone(this.current); - const userMessage = document.getElementById('user-message'); - let errorMessage = null; - - // User must choose a name for their file - if ( - !settings.name || - settings.name.toString().trim().length === 0 || - settings.name.toString().toLowerCase().trim() === 'player' - ) { - userMessage.innerText = 'You forgot to set your player name at the top of the page!'; - userMessage.classList.add('visible'); - userMessage.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - 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; - } - - Object.keys(settings[game]).forEach((setting) => { - // Remove any disabled options - Object.keys(settings[game][setting]).forEach((option) => { - if (settings[game][setting][option] === 0) { - 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!`; - } - - // Remove weights from options with only one possibility - if ( - Object.keys(settings[game][setting]).length === 1 && - !Array.isArray(settings[game][setting]) && - setting !== 'start_inventory' - ) { - settings[game][setting] = Object.keys(settings[game][setting])[0]; - } - - // Remove empty arrays - else if ( - ['exclude_locations', 'priority_locations', 'local_items', - 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && - settings[game][setting].length === 0 - ) { - delete settings[game][setting]; - } - - // Remove empty start inventory - else if ( - setting === 'start_inventory' && - Object.keys(settings[game]['start_inventory']).length === 0 - ) { - delete settings[game]['start_inventory']; - } - }); - }); - - if (Object.keys(settings.game).length === 0) { - errorMessage = 'You have not chosen a game to play!'; - } - - // Remove weights if there is only one game - else if (Object.keys(settings.game).length === 1) { - settings.game = Object.keys(settings.game)[0]; - } - - // 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; - } - - updateVisibleGames() { - Object.entries(this.current.game).forEach(([game, weight]) => { - const gameDiv = document.getElementById(`${game}-div`); - const gameOption = document.getElementById(`${game}-game-option`); - if (parseInt(weight, 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'); - } - }); - } - - updateBaseSetting(event) { - 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': - this.current[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': - this.current[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); - break; - } - - this.save(); - } - - export() { - const settings = this.#validateSettings(); - if (!settings) { return; } - - const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); - download(`${document.getElementById('player-name').value}.yaml`, yamlText); - } - - generateGame(raceMode = false) { - const settings = this.#validateSettings(); - if (!settings) { return; } - - axios.post('/api/generate', { - weights: { player: JSON.stringify(settings) }, - presetData: { player: JSON.stringify(settings) }, - playerCount: 1, - spoiler: 3, - 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'); - userMessage.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - console.error(error); - }); - } -} - -// Settings for an individual game. -class GameSettings { - // The WeightedSettings that contains this game's settings. Used to save - // settings after editing. - #allSettings; - - // The name of this game. - name; - - // The data from the server describing the types of settings available for - // this game, as a JSON-safe blob. - get data() { - return this.#allSettings.data.games[this.name]; - } - - // The settings chosen by the user as they'd appear in the YAML file, stored - // to and retrieved from local storage. - get current() { - return this.#allSettings.current[this.name]; - } - - constructor(allSettings, name) { - this.#allSettings = allSettings; - this.name = name; - } - - // Builds and returns the settings UI for this game. - buildUI() { - // Create game div, invisible by default - const gameDiv = document.createElement('div'); - gameDiv.setAttribute('id', `${this.name}-div`); - gameDiv.classList.add('game-div'); - gameDiv.classList.add('invisible'); - - const gameHeader = document.createElement('h2'); - gameHeader.innerText = this.name; - 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); - - // Sort items and locations alphabetically. - this.data.gameItems.sort(); - this.data.gameLocations.sort(); - - const weightedSettingsDiv = this.#buildWeightedSettingsDiv(); - gameDiv.appendChild(weightedSettingsDiv); - - const itemPoolDiv = this.#buildItemPoolDiv(); - gameDiv.appendChild(itemPoolDiv); - - const hintsDiv = this.#buildHintsDiv(); - gameDiv.appendChild(hintsDiv); - - const locationsDiv = this.#buildPriorityExclusionDiv(); - gameDiv.appendChild(locationsDiv); - - collapseButton.addEventListener('click', () => { - collapseButton.classList.add('invisible'); - weightedSettingsDiv.classList.add('invisible'); - itemPoolDiv.classList.add('invisible'); - hintsDiv.classList.add('invisible'); - locationsDiv.classList.add('invisible'); - expandButton.classList.remove('invisible'); - }); - - expandButton.addEventListener('click', () => { - collapseButton.classList.remove('invisible'); - weightedSettingsDiv.classList.remove('invisible'); - itemPoolDiv.classList.remove('invisible'); - hintsDiv.classList.remove('invisible'); - locationsDiv.classList.remove('invisible'); - expandButton.classList.add('invisible'); - }); - - return gameDiv; - } - - #buildWeightedSettingsDiv() { - const settingsWrapper = document.createElement('div'); - settingsWrapper.classList.add('settings-wrapper'); - - Object.keys(this.data.gameSettings).forEach((settingName) => { - const setting = this.data.gameSettings[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', this.name); - 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', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][option.value]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${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': - case 'named_range': - const rangeTable = document.createElement('table'); - const rangeTbody = document.createElement('tbody'); - - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + - `below, then press the "Add" button to add a weight for it.

Accepted values:
` + - `Normal range: ${setting.min} - ${setting.max}`; - - const acceptedValuesOutsideRange = []; - if (setting.hasOwnProperty('value_names')) { - Object.keys(setting.value_names).forEach((specialName) => { - if ( - (setting.value_names[specialName] < setting.min) || - (setting.value_names[specialName] > setting.max) - ) { - hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; - acceptedValuesOutsideRange.push(setting.value_names[specialName]); - } - }); - - hintText.innerHTML += '

Certain values have special meaning:'; - Object.keys(setting.value_names).forEach((specialName) => { - hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`; - }); - } - - settingWrapper.appendChild(hintText); - - const addOptionDiv = document.createElement('div'); - addOptionDiv.classList.add('add-option-div'); - const optionInput = document.createElement('input'); - optionInput.setAttribute('id', `${this.name}-${settingName}-option`); - let placeholderText = `${setting.min} - ${setting.max}`; - acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`); - optionInput.setAttribute('placeholder', placeholderText); - 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')); } - }); - - addOptionButton.addEventListener('click', () => { - const optionInput = document.getElementById(`${this.name}-${settingName}-option`); - let option = optionInput.value; - if (!option || !option.trim()) { return; } - option = parseInt(option, 10); - - let optionAcceptable = false; - if ((option >= setting.min) && (option <= setting.max)) { - optionAcceptable = true; - } - if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){ - optionAcceptable = true; - } - if (!optionAcceptable) { return; } - - optionInput.value = ''; - if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - if ( - setting.hasOwnProperty('value_names') && - Object.values(setting.value_names).includes(parseInt(option, 10)) - ) { - const optionName = Object.keys(setting.value_names).find( - (key) => setting.value_names[key] === parseInt(option, 10) - ); - tdLeft.innerText += ` [${optionName}]`; - } - 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', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${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); - - // Save new option to settings - range.dispatchEvent(new Event('change')); - }); - - Object.keys(this.current[settingName]).forEach((option) => { - // These options are statically generated below, and should always appear even if they are deleted - // from localStorage - if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - if ( - setting.hasOwnProperty('value_names') && - Object.values(setting.value_names).includes(parseInt(option, 10)) - ) { - const optionName = Object.keys(setting.value_names).find( - (key) => setting.value_names[key] === parseInt(option, 10) - ); - tdLeft.innerText += ` [${optionName}]`; - } - 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', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${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; - const changeEvent = new Event('change'); - changeEvent.action = 'rangeDelete'; - range.dispatchEvent(changeEvent); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - - rangeTbody.appendChild(tr); - }); - - ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - switch(option){ - case 'random': - tdLeft.innerText = 'Random'; - break; - case 'random-low': - tdLeft.innerText = "Random (Low)"; - break; - case 'random-middle': - tdLeft.innerText = 'Random (Middle)'; - break; - case 'random-high': - tdLeft.innerText = "Random (High)"; - break; - } - 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', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][option]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - rangeTbody.appendChild(tr); - }); - - rangeTable.appendChild(rangeTbody); - settingWrapper.appendChild(rangeTable); - break; - - case 'items-list': - const itemsList = this.#buildItemsDiv(settingName); - settingWrapper.appendChild(itemsList); - break; - - case 'locations-list': - const locationsList = this.#buildLocationsDiv(settingName); - settingWrapper.appendChild(locationsList); - break; - - case 'custom-list': - const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options); - settingWrapper.appendChild(customList); - break; - - default: - console.error(`Unknown setting type for ${this.name} setting ${settingName}: ${setting.type}`); - return; - } - - settingsWrapper.appendChild(settingWrapper); - }); - - return settingsWrapper; - } - - #buildItemPoolDiv() { - const itemsDiv = document.createElement('div'); - itemsDiv.classList.add('items-div'); - - const itemsDivHeader = document.createElement('h3'); - itemsDivHeader.innerText = 'Item Pool'; - itemsDiv.appendChild(itemsDivHeader); - - const itemsDescription = document.createElement('p'); - itemsDescription.classList.add('setting-description'); - itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' + - 'your seed or someone else\'s.'; - itemsDiv.appendChild(itemsDescription); - - const itemsHint = document.createElement('p'); - itemsHint.classList.add('hint-text'); - itemsHint.innerText = 'Drag and drop items from one box to another.'; - itemsDiv.appendChild(itemsHint); - - const itemsWrapper = document.createElement('div'); - itemsWrapper.classList.add('items-wrapper'); - - const itemDragoverHandler = (evt) => evt.preventDefault(); - const itemDropHandler = (evt) => this.#itemDropHandler(evt); - - // Create container divs for each category - const availableItemsWrapper = document.createElement('div'); - availableItemsWrapper.classList.add('item-set-wrapper'); - availableItemsWrapper.innerText = 'Available Items'; - const availableItems = document.createElement('div'); - availableItems.classList.add('item-container'); - availableItems.setAttribute('id', `${this.name}-available_items`); - availableItems.addEventListener('dragover', itemDragoverHandler); - availableItems.addEventListener('drop', itemDropHandler); - - const startInventoryWrapper = document.createElement('div'); - startInventoryWrapper.classList.add('item-set-wrapper'); - startInventoryWrapper.innerText = 'Start Inventory'; - const startInventory = document.createElement('div'); - startInventory.classList.add('item-container'); - startInventory.setAttribute('id', `${this.name}-start_inventory`); - startInventory.setAttribute('data-setting', 'start_inventory'); - startInventory.addEventListener('dragover', itemDragoverHandler); - startInventory.addEventListener('drop', itemDropHandler); - - const localItemsWrapper = document.createElement('div'); - localItemsWrapper.classList.add('item-set-wrapper'); - localItemsWrapper.innerText = 'Local Items'; - const localItems = document.createElement('div'); - localItems.classList.add('item-container'); - localItems.setAttribute('id', `${this.name}-local_items`); - localItems.setAttribute('data-setting', 'local_items') - localItems.addEventListener('dragover', itemDragoverHandler); - localItems.addEventListener('drop', itemDropHandler); - - const nonLocalItemsWrapper = document.createElement('div'); - nonLocalItemsWrapper.classList.add('item-set-wrapper'); - nonLocalItemsWrapper.innerText = 'Non-Local Items'; - const nonLocalItems = document.createElement('div'); - nonLocalItems.classList.add('item-container'); - nonLocalItems.setAttribute('id', `${this.name}-non_local_items`); - nonLocalItems.setAttribute('data-setting', 'non_local_items'); - nonLocalItems.addEventListener('dragover', itemDragoverHandler); - nonLocalItems.addEventListener('drop', itemDropHandler); - - // Populate the divs - this.data.gameItems.forEach((item) => { - if (Object.keys(this.current.start_inventory).includes(item)){ - const itemDiv = this.#buildItemQtyDiv(item); - itemDiv.setAttribute('data-setting', 'start_inventory'); - startInventory.appendChild(itemDiv); - } else if (this.current.local_items.includes(item)) { - const itemDiv = this.#buildItemDiv(item); - itemDiv.setAttribute('data-setting', 'local_items'); - localItems.appendChild(itemDiv); - } else if (this.current.non_local_items.includes(item)) { - const itemDiv = this.#buildItemDiv(item); - itemDiv.setAttribute('data-setting', 'non_local_items'); - nonLocalItems.appendChild(itemDiv); - } else { - const itemDiv = this.#buildItemDiv(item); - availableItems.appendChild(itemDiv); - } - }); - - availableItemsWrapper.appendChild(availableItems); - startInventoryWrapper.appendChild(startInventory); - localItemsWrapper.appendChild(localItems); - nonLocalItemsWrapper.appendChild(nonLocalItems); - itemsWrapper.appendChild(availableItemsWrapper); - itemsWrapper.appendChild(startInventoryWrapper); - itemsWrapper.appendChild(localItemsWrapper); - itemsWrapper.appendChild(nonLocalItemsWrapper); - itemsDiv.appendChild(itemsWrapper); - return itemsDiv; - } - - #buildItemDiv(item) { - const itemDiv = document.createElement('div'); - itemDiv.classList.add('item-div'); - itemDiv.setAttribute('id', `${this.name}-${item}`); - itemDiv.setAttribute('data-game', this.name); - itemDiv.setAttribute('data-item', item); - itemDiv.setAttribute('draggable', 'true'); - itemDiv.innerText = item; - itemDiv.addEventListener('dragstart', (evt) => { - evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id')); - }); - return itemDiv; - } - - #buildItemQtyDiv(item) { - const itemQtyDiv = document.createElement('div'); - itemQtyDiv.classList.add('item-qty-div'); - itemQtyDiv.setAttribute('id', `${this.name}-${item}`); - itemQtyDiv.setAttribute('data-game', this.name); - 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', this.current.start_inventory.hasOwnProperty(item) ? - this.current.start_inventory[item] : '1'); - itemQty.setAttribute('data-game', this.name); - 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); - this.#updateItemSetting(evt); - }); - inputWrapper.appendChild(itemQty); - itemQtyDiv.appendChild(inputWrapper); - - itemQtyDiv.addEventListener('dragstart', (evt) => { - evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id')); - }); - return itemQtyDiv; - } - - #itemDropHandler(evt) { - evt.preventDefault(); - const sourceId = evt.dataTransfer.getData('text/plain'); - const sourceDiv = document.getElementById(sourceId); - - const item = sourceDiv.getAttribute('data-item'); - - 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 itemDiv = newSetting === 'start_inventory' ? this.#buildItemQtyDiv(item) : this.#buildItemDiv(item); - - if (oldSetting) { - if (oldSetting === 'start_inventory') { - if (this.current[oldSetting].hasOwnProperty(item)) { - delete this.current[oldSetting][item]; - } - } else { - if (this.current[oldSetting].includes(item)) { - this.current[oldSetting].splice(this.current[oldSetting].indexOf(item), 1); - } - } - } - - if (newSetting) { - itemDiv.setAttribute('data-setting', newSetting); - document.getElementById(`${this.name}-${newSetting}`).appendChild(itemDiv); - if (newSetting === 'start_inventory') { - this.current[newSetting][item] = 1; - } else { - if (!this.current[newSetting].includes(item)){ - this.current[newSetting].push(item); - } - } - } else { - // No setting was assigned, this item has been removed from the settings - document.getElementById(`${this.name}-available_items`).appendChild(itemDiv); - } - - // Remove the source drag object - sourceDiv.parentElement.removeChild(sourceDiv); - - // Save the updated settings - this.save(); - } - - #buildHintsDiv() { - 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.'; - hintsDiv.appendChild(hintsDescription); - - const itemHintsContainer = document.createElement('div'); - itemHintsContainer.classList.add('hints-container'); - - // Item Hints - const itemHintsWrapper = document.createElement('div'); - itemHintsWrapper.classList.add('hints-wrapper'); - itemHintsWrapper.innerText = 'Starting Item Hints'; - - const itemHintsDiv = this.#buildItemsDiv('start_hints'); - itemHintsWrapper.appendChild(itemHintsDiv); - itemHintsContainer.appendChild(itemHintsWrapper); - - // Starting Location Hints - const locationHintsWrapper = document.createElement('div'); - locationHintsWrapper.classList.add('hints-wrapper'); - locationHintsWrapper.innerText = 'Starting Location Hints'; - - const locationHintsDiv = this.#buildLocationsDiv('start_location_hints'); - locationHintsWrapper.appendChild(locationHintsDiv); - itemHintsContainer.appendChild(locationHintsWrapper); - - hintsDiv.appendChild(itemHintsContainer); - return hintsDiv; - } - - #buildPriorityExclusionDiv() { - const locationsDiv = document.createElement('div'); - locationsDiv.classList.add('locations-div'); - const locationsHeader = document.createElement('h3'); - locationsHeader.innerText = 'Priority & Exclusion Locations'; - locationsDiv.appendChild(locationsHeader); - const locationsDescription = document.createElement('p'); - locationsDescription.classList.add('setting-description'); - locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' + - 'excluded locations will not contain progression or useful items.'; - locationsDiv.appendChild(locationsDescription); - - const locationsContainer = document.createElement('div'); - locationsContainer.classList.add('locations-container'); - - // Priority Locations - const priorityLocationsWrapper = document.createElement('div'); - priorityLocationsWrapper.classList.add('locations-wrapper'); - priorityLocationsWrapper.innerText = 'Priority Locations'; - - const priorityLocationsDiv = this.#buildLocationsDiv('priority_locations'); - priorityLocationsWrapper.appendChild(priorityLocationsDiv); - locationsContainer.appendChild(priorityLocationsWrapper); - - // Exclude Locations - const excludeLocationsWrapper = document.createElement('div'); - excludeLocationsWrapper.classList.add('locations-wrapper'); - excludeLocationsWrapper.innerText = 'Exclude Locations'; - - const excludeLocationsDiv = this.#buildLocationsDiv('exclude_locations'); - excludeLocationsWrapper.appendChild(excludeLocationsDiv); - locationsContainer.appendChild(excludeLocationsWrapper); - - locationsDiv.appendChild(locationsContainer); - return locationsDiv; - } - - // Builds a div for a setting whose value is a list of locations. - #buildLocationsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameLocations, { - groups: this.data.gameLocationGroups, - descriptions: this.data.gameLocationDescriptions, - }); - } - - // Builds a div for a setting whose value is a list of items. - #buildItemsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameItems, { - groups: this.data.gameItemGroups, - descriptions: this.data.gameItemDescriptions - }); - } - - // Builds a div for a setting named `setting` with a list value that can - // contain `items`. - // - // The `groups` option can be a list of additional options for this list - // (usually `item_name_groups` or `location_name_groups`) that are displayed - // in a special section at the top of the list. - // - // The `descriptions` option can be a map from item names or group names to - // descriptions for the user's benefit. - #buildListDiv(setting, items, {groups = [], descriptions = {}} = {}) { - const div = document.createElement('div'); - div.classList.add('simple-list'); - - groups.forEach((group) => { - const row = this.#addListRow(setting, group, descriptions[group]); - div.appendChild(row); - }); - - if (groups.length > 0) { - div.appendChild(document.createElement('hr')); - } - - items.forEach((item) => { - const row = this.#addListRow(setting, item, descriptions[item]); - div.appendChild(row); - }); - - return div; - } - - // Builds and returns a row for a list of checkboxes. - // - // If `help` is passed, it's displayed as a help tooltip for this list item. - #addListRow(setting, item, help = undefined) { - const row = document.createElement('div'); - row.classList.add('list-row'); - - const label = document.createElement('label'); - label.setAttribute('for', `${this.name}-${setting}-${item}`); - - const checkbox = document.createElement('input'); - checkbox.setAttribute('type', 'checkbox'); - checkbox.setAttribute('id', `${this.name}-${setting}-${item}`); - checkbox.setAttribute('data-game', this.name); - checkbox.setAttribute('data-setting', setting); - checkbox.setAttribute('data-option', item); - if (this.current[setting].includes(item)) { - checkbox.setAttribute('checked', '1'); - } - checkbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - label.appendChild(checkbox); - - const name = document.createElement('span'); - name.innerText = item; - - if (help) { - const helpSpan = document.createElement('span'); - helpSpan.classList.add('interactive'); - helpSpan.setAttribute('data-tooltip', help); - helpSpan.innerText = '(?)'; - name.innerText += ' '; - name.appendChild(helpSpan); - - // Put the first 7 tooltips below their rows. CSS tooltips in scrolling - // containers can't be visible outside those containers, so this helps - // ensure they won't be pushed out the top. - if (helpSpan.parentNode.childNodes.length < 7) { - helpSpan.classList.add('tooltip-bottom'); - } - } - - label.appendChild(name); - - row.appendChild(label); - return row; - } - - #updateRangeSetting(evt) { - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - document.getElementById(`${this.name}-${setting}-${option}`).innerText = evt.target.value; - if (evt.action && evt.action === 'rangeDelete') { - delete this.current[setting][option]; - } else { - this.current[setting][option] = parseInt(evt.target.value, 10); - } - this.save(); - } - - #updateListSetting(evt) { - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - - if (evt.target.checked) { - // If the option is to be enabled and it is already enabled, do nothing - if (this.current[setting].includes(option)) { return; } - - this.current[setting].push(option); - } else { - // If the option is to be disabled and it is already disabled, do nothing - if (!this.current[setting].includes(option)) { return; } - - this.current[setting].splice(this.current[setting].indexOf(option), 1); - } - this.save(); - } - - #updateItemSetting(evt) { - const setting = evt.target.getAttribute('data-setting'); - const option = evt.target.getAttribute('data-option'); - if (setting === 'start_inventory') { - this.current[setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0; - } else { - this.current[setting][option] = isNaN(evt.target.value) ? - evt.target.value : parseInt(evt.target.value, 10); - } - this.save(); - } - - // Saves the current settings to local storage. - save() { - this.#allSettings.save(); - } -} - -/** 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); -}; diff --git a/WebHostLib/static/assets/weightedOptions.js b/WebHostLib/static/assets/weightedOptions.js new file mode 100644 index 00000000..0417ab17 --- /dev/null +++ b/WebHostLib/static/assets/weightedOptions.js @@ -0,0 +1,223 @@ +let deletedOptions = {}; + +window.addEventListener('load', () => { + const worldName = document.querySelector('#weighted-options').getAttribute('data-game'); + + // Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time + // and handles dynamically created elements + document.addEventListener('change', (evt) => { + // Handle updates to range inputs + if (evt.target.type === 'range') { + // Update span containing range value. All ranges have a corresponding `{rangeId}-value` span + document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value; + + // If the changed option was the name of a game, determine whether to show or hide that game's div + if (evt.target.id.startsWith('game||')) { + const gameName = evt.target.id.split('||')[1]; + const gameDiv = document.getElementById(`${gameName}-container`); + if (evt.target.value > 0) { + gameDiv.classList.remove('hidden'); + } else { + gameDiv.classList.add('hidden'); + } + } + } + }); + + // Generic click listener + document.addEventListener('click', (evt) => { + // Handle creating new rows for Range options + if (evt.target.classList.contains('add-range-option-button')) { + const optionName = evt.target.getAttribute('data-option'); + addRangeRow(optionName); + } + + // Handle deleting range rows + if (evt.target.classList.contains('range-option-delete')) { + const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`); + setDeletedOption( + targetRow.getAttribute('data-option-name'), + targetRow.getAttribute('data-value'), + ); + targetRow.parentElement.removeChild(targetRow); + } + }); + + // Listen for enter presses on inputs intended to add range rows + document.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { + evt.preventDefault(); + } + + if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) { + const optionName = evt.target.getAttribute('data-option'); + addRangeRow(optionName); + } + }); + + // Detect form submission + document.getElementById('weighted-options-form').addEventListener('submit', (evt) => { + // Save data to localStorage + const weightedOptions = {}; + document.querySelectorAll('input[name]').forEach((input) => { + const keys = input.getAttribute('name').split('||'); + + // Determine keys + const optionName = keys[0] ?? null; + const subOption = keys[1] ?? null; + + // Ensure keys exist + if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; } + if (subOption && !weightedOptions[optionName][subOption]) { + weightedOptions[optionName][subOption] = null; + } + + if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); } + if (optionName) { return weightedOptions[optionName] = determineValue(input); } + }); + + localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions)); + localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions)); + }); + + // Remove all deleted values as specified by localStorage + deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}'); + Object.keys(deletedOptions).forEach((optionName) => { + deletedOptions[optionName].forEach((value) => { + const targetRow = document.querySelector(`tr[data-row="${value}-row"]`); + targetRow.parentElement.removeChild(targetRow); + }); + }); + + // Populate all settings from localStorage on page initialisation + const previousSettingsJson = localStorage.getItem(`${worldName}-weights`); + if (previousSettingsJson) { + const previousSettings = JSON.parse(previousSettingsJson); + Object.keys(previousSettings).forEach((option) => { + if (typeof previousSettings[option] === 'string') { + return document.querySelector(`input[name="${option}"]`).value = previousSettings[option]; + } + + Object.keys(previousSettings[option]).forEach((value) => { + const input = document.querySelector(`input[name="${option}||${value}"]`); + if (!input?.type) { + return console.error(`Unable to populate option with name ${option}||${value}.`); + } + + switch (input.type) { + case 'checkbox': + input.checked = (parseInt(previousSettings[option][value], 10) === 1); + break; + case 'range': + input.value = parseInt(previousSettings[option][value], 10); + break; + case 'number': + input.value = previousSettings[option][value].toString(); + break; + default: + console.error(`Found unsupported input type: ${input.type}`); + } + }); + }); + } +}); + +const addRangeRow = (optionName) => { + const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`; + const inputTarget = document.querySelector(inputQuery); + const newValue = inputTarget.value; + if (!/^-?\d+$/.test(newValue)) { + alert('Range values must be a positive or negative integer!'); + return; + } + inputTarget.value = ''; + const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`); + const tr = document.createElement('tr'); + tr.setAttribute('data-row', `${optionName}-${newValue}-row`); + tr.setAttribute('data-option-name', optionName); + tr.setAttribute('data-value', newValue); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + const label = document.createElement('label'); + label.setAttribute('for', `${optionName}||${newValue}`); + label.innerText = newValue.toString(); + tdLeft.appendChild(label); + 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('value', '0'); + range.setAttribute('id', `${optionName}||${newValue}`); + range.setAttribute('name', `${optionName}||${newValue}`); + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + const tdRight = document.createElement('td'); + tdRight.classList.add('td-right'); + const valueSpan = document.createElement('span'); + valueSpan.setAttribute('id', `${optionName}||${newValue}-value`); + valueSpan.innerText = '0'; + tdRight.appendChild(valueSpan); + tr.appendChild(tdRight); + const tdDelete = document.createElement('td'); + const deleteSpan = document.createElement('span'); + deleteSpan.classList.add('range-option-delete'); + deleteSpan.classList.add('js-required'); + deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`); + deleteSpan.innerText = '❌'; + tdDelete.appendChild(deleteSpan); + tr.appendChild(tdDelete); + tBody.appendChild(tr); + + // Remove this option from the set of deleted options if it exists + unsetDeletedOption(optionName, newValue); +}; + +/** + * Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox + * + * @param {object} input - The input element. + * @returns {number} The value of the input element. + */ +const determineValue = (input) => { + switch (input.type) { + case 'checkbox': + return (input.checked ? 1 : 0); + case 'range': + return parseInt(input.value, 10); + default: + return input.value; + } +}; + +/** + * Sets the deleted option value for a given world and option name. + * If the world or option does not exist, it creates the necessary entries. + * + * @param {string} optionName - The name of the option. + * @param {*} value - The value to be set for the deleted option. + * @returns {void} + */ +const setDeletedOption = (optionName, value) => { + deletedOptions[optionName] = deletedOptions[optionName] || []; + deletedOptions[optionName].push(`${optionName}-${value}`); +}; + +/** + * Removes a specific value from the deletedOptions object. + * + * @param {string} optionName - The name of the option. + * @param {*} value - The value to be removed + * @returns {void} + */ +const unsetDeletedOption = (optionName, value) => { + if (!deletedOptions.hasOwnProperty(optionName)) { return; } + if (deletedOptions[optionName].includes(`${optionName}-${value}`)) { + deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1); + } + if (deletedOptions[optionName].length === 0) { + delete deletedOptions[optionName]; + } +}; diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css index a787b0c6..1a014483 100644 --- a/WebHostLib/static/styles/globalStyles.css +++ b/WebHostLib/static/styles/globalStyles.css @@ -44,7 +44,7 @@ a{ font-family: LexendDeca-Regular, sans-serif; } -button{ +button, input[type=submit]{ font-weight: 500; font-size: 0.9rem; padding: 10px 17px 11px 16px; /* top right bottom left */ @@ -57,7 +57,7 @@ button{ cursor: pointer; } -button:active{ +button:active, input[type=submit]:active{ border-right: 1px solid rgba(0, 0, 0, 0.5); border-bottom: 1px solid rgba(0, 0, 0, 0.5); padding-right: 16px; @@ -66,11 +66,11 @@ button:active{ margin-bottom: 2px; } -button.button-grass{ +button.button-grass, input[type=submit].button-grass{ border: 1px solid black; } -button.button-dirt{ +button.button-dirt, input[type=submit].button-dirt{ border: 1px solid black; } @@ -111,4 +111,4 @@ h5, h6{ .interactive{ color: #ffef00; -} \ No newline at end of file +} diff --git a/WebHostLib/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css index dce13558..e0165b74 100644 --- a/WebHostLib/static/styles/markdown.css +++ b/WebHostLib/static/styles/markdown.css @@ -23,7 +23,7 @@ .markdown a{} -.markdown h1{ +.markdown h1, .markdown details summary.h1{ font-size: 52px; font-weight: normal; font-family: LondrinaSolid-Regular, sans-serif; @@ -33,7 +33,7 @@ text-shadow: 1px 1px 4px #000000; } -.markdown h2{ +.markdown h2, .markdown details summary.h2{ font-size: 38px; font-weight: normal; font-family: LondrinaSolid-Light, sans-serif; @@ -45,7 +45,7 @@ text-shadow: 1px 1px 2px #000000; } -.markdown h3{ +.markdown h3, .markdown details summary.h3{ font-size: 26px; font-family: LexendDeca-Regular, sans-serif; text-transform: none; @@ -55,7 +55,7 @@ margin-bottom: 0.5rem; } -.markdown h4{ +.markdown h4, .markdown details summary.h4{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 24px; @@ -63,21 +63,21 @@ margin-bottom: 24px; } -.markdown h5{ +.markdown h5, .markdown details summary.h5{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 22px; cursor: pointer; } -.markdown h6{ +.markdown h6, .markdown details summary.h6{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 20px; cursor: pointer;; } -.markdown h4, .markdown h5,.markdown h6{ +.markdown h4, .markdown h5, .markdown h6{ margin-bottom: 0.5rem; } diff --git a/WebHostLib/static/styles/player-options.css b/WebHostLib/static/styles/player-options.css deleted file mode 100644 index cc2d5e2d..00000000 --- a/WebHostLib/static/styles/player-options.css +++ /dev/null @@ -1,244 +0,0 @@ -html{ - background-image: url('../static/backgrounds/grass.png'); - background-repeat: repeat; - background-size: 650px 650px; -} - -#player-options{ - box-sizing: border-box; - max-width: 1024px; - margin-left: auto; - margin-right: auto; - background-color: rgba(0, 0, 0, 0.15); - border-radius: 8px; - padding: 1rem; - color: #eeffeb; -} - -#player-options #player-options-button-row{ - display: flex; - flex-direction: row; - justify-content: space-between; - margin-top: 15px; -} - -#player-options code{ - background-color: #d9cd8e; - border-radius: 4px; - padding-left: 0.25rem; - padding-right: 0.25rem; - color: #000000; -} - -#player-options #user-message{ - display: none; - width: calc(100% - 8px); - background-color: #ffe86b; - border-radius: 4px; - color: #000000; - padding: 4px; - text-align: center; -} - -#player-options #user-message.visible{ - display: block; - cursor: pointer; -} - -#player-options h1{ - font-size: 2.5rem; - font-weight: normal; - width: 100%; - margin-bottom: 0.5rem; - text-shadow: 1px 1px 4px #000000; -} - -#player-options h2{ - font-size: 40px; - font-weight: normal; - width: 100%; - margin-bottom: 0.5rem; - text-transform: lowercase; - text-shadow: 1px 1px 2px #000000; -} - -#player-options h3, #player-options h4, #player-options h5, #player-options h6{ - text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); -} - -#player-options input:not([type]){ - border: 1px solid #000000; - padding: 3px; - border-radius: 3px; - min-width: 150px; -} - -#player-options input:not([type]):focus{ - border: 1px solid #ffffff; -} - -#player-options select{ - border: 1px solid #000000; - padding: 3px; - border-radius: 3px; - min-width: 150px; - background-color: #ffffff; -} - -#player-options #game-options, #player-options #rom-options{ - display: flex; - flex-direction: row; -} - -#player-options #meta-options { - display: flex; - justify-content: space-between; - gap: 20px; - padding: 3px; -} - -#player-options div { - display: flex; - flex-grow: 1; -} - -#player-options #meta-options label { - display: inline-block; - min-width: 180px; - flex-grow: 1; -} - -#player-options #meta-options input, -#player-options #meta-options select { - box-sizing: border-box; - min-width: 150px; - width: 50%; -} - -#player-options .left, #player-options .right{ - flex-grow: 1; -} - -#player-options .left{ - margin-right: 10px; -} - -#player-options .right{ - margin-left: 10px; -} - -#player-options table{ - margin-bottom: 30px; - width: 100%; -} - -#player-options table .select-container{ - display: flex; - flex-direction: row; -} - -#player-options table .select-container select{ - min-width: 200px; - flex-grow: 1; -} - -#player-options table select:disabled{ - background-color: lightgray; -} - -#player-options table .range-container{ - display: flex; - flex-direction: row; -} - -#player-options table .range-container input[type=range]{ - flex-grow: 1; -} - -#player-options table .range-value{ - min-width: 20px; - margin-left: 0.25rem; -} - -#player-options table .named-range-container{ - display: flex; - flex-direction: column; -} - -#player-options table .named-range-wrapper{ - display: flex; - flex-direction: row; - margin-top: 0.25rem; -} - -#player-options table .named-range-wrapper input[type=range]{ - flex-grow: 1; -} - -#player-options table .randomize-button { - max-height: 24px; - line-height: 16px; - padding: 2px 8px; - margin: 0 0 0 0.25rem; - font-size: 12px; - border: 1px solid black; - border-radius: 3px; -} - -#player-options table .randomize-button.active { - background-color: #ffef00; /* Same as .interactive in globalStyles.css */ -} - -#player-options table .randomize-button[data-tooltip]::after { - left: unset; - right: 0; -} - -#player-options table label{ - display: block; - min-width: 200px; - margin-right: 4px; - cursor: default; -} - -#player-options th, #player-options td{ - border: none; - padding: 3px; - font-size: 17px; - vertical-align: top; -} - -@media all and (max-width: 1024px) { - #player-options { - border-radius: 0; - } - - #player-options #meta-options { - flex-direction: column; - justify-content: flex-start; - gap: 6px; - } - - #player-options #game-options{ - justify-content: flex-start; - flex-wrap: wrap; - } - - #player-options .left, - #player-options .right { - margin: 0; - } - - #game-options table { - margin-bottom: 0; - } - - #game-options table label{ - display: block; - min-width: 200px; - } - - #game-options table tr td { - width: 50%; - } -} diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css b/WebHostLib/static/styles/playerOptions/playerOptions.css new file mode 100644 index 00000000..6165e3a0 --- /dev/null +++ b/WebHostLib/static/styles/playerOptions/playerOptions.css @@ -0,0 +1,310 @@ +@import "../markdown.css"; +html { + background-image: url("../../static/backgrounds/grass.png"); + background-repeat: repeat; + background-size: 650px 650px; + overflow-x: hidden; +} + +#player-options { + box-sizing: border-box; + max-width: 1024px; + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: #eeffeb; + word-break: break-all; +} +#player-options #player-options-header h1 { + margin-bottom: 0; + padding-bottom: 0; +} +#player-options #player-options-header h1:nth-child(2) { + font-size: 1.4rem; + margin-top: -8px; + margin-bottom: 0.5rem; +} +#player-options .js-warning-banner { + width: calc(100% - 1rem); + padding: 0.5rem; + border-radius: 4px; + background-color: #f3f309; + color: #000000; + margin-bottom: 0.5rem; + text-align: center; +} +#player-options .group-container { + padding: 0; + margin: 0; +} +#player-options .group-container h2 { + user-select: none; + cursor: unset; +} +#player-options .group-container h2 label { + cursor: pointer; +} +#player-options #player-options-button-row { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; +} +#player-options #user-message { + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; + cursor: pointer; +} +#player-options h1 { + font-size: 2.5rem; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + text-shadow: 1px 1px 4px #000000; +} +#player-options h2 { + font-size: 40px; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + text-transform: lowercase; + text-shadow: 1px 1px 2px #000000; +} +#player-options h3, #player-options h4, #player-options h5, #player-options h6 { + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} +#player-options input:not([type]) { + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; +} +#player-options input:not([type]):focus { + border: 1px solid #ffffff; +} +#player-options select { + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + background-color: #ffffff; + text-overflow: ellipsis; +} +#player-options .game-options { + display: flex; + flex-direction: row; +} +#player-options .game-options .left, #player-options .game-options .right { + display: grid; + grid-template-columns: 12rem auto; + grid-row-gap: 0.5rem; + grid-auto-rows: min-content; + align-items: start; + min-width: 480px; + width: 50%; +} +#player-options #meta-options { + display: flex; + justify-content: space-between; + gap: 20px; + padding: 3px; +} +#player-options #meta-options input, #player-options #meta-options select { + box-sizing: border-box; + width: 200px; +} +#player-options .left, #player-options .right { + flex-grow: 1; + margin-bottom: 0.5rem; +} +#player-options .left { + margin-right: 20px; +} +#player-options .select-container { + display: flex; + flex-direction: row; + max-width: 270px; +} +#player-options .select-container select { + min-width: 200px; + flex-grow: 1; +} +#player-options .select-container select:disabled { + background-color: lightgray; +} +#player-options .range-container { + display: flex; + flex-direction: row; + max-width: 270px; +} +#player-options .range-container input[type=range] { + flex-grow: 1; +} +#player-options .range-container .range-value { + min-width: 20px; + margin-left: 0.25rem; +} +#player-options .named-range-container { + display: flex; + flex-direction: column; + max-width: 270px; +} +#player-options .named-range-container .named-range-wrapper { + display: flex; + flex-direction: row; + margin-top: 0.25rem; +} +#player-options .named-range-container .named-range-wrapper input[type=range] { + flex-grow: 1; +} +#player-options .free-text-container { + display: flex; + flex-direction: column; + max-width: 270px; +} +#player-options .free-text-container input[type=text] { + flex-grow: 1; +} +#player-options .text-choice-container { + display: flex; + flex-direction: column; + max-width: 270px; +} +#player-options .text-choice-container .text-choice-wrapper { + display: flex; + flex-direction: row; + margin-bottom: 0.25rem; +} +#player-options .text-choice-container .text-choice-wrapper select { + flex-grow: 1; +} +#player-options .option-container { + display: flex; + flex-direction: column; + background-color: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(20, 20, 20, 0.25); + border-radius: 3px; + color: #ffffff; + max-height: 10rem; + min-width: 14.5rem; + overflow-y: auto; + padding-right: 0.25rem; + padding-left: 0.25rem; +} +#player-options .option-container .option-divider { + width: 100%; + height: 2px; + background-color: rgba(20, 20, 20, 0.25); + margin-top: 0.125rem; + margin-bottom: 0.125rem; +} +#player-options .option-container .option-entry { + display: flex; + flex-direction: row; + align-items: flex-start; + margin-bottom: 0.125rem; + margin-top: 0.125rem; + user-select: none; +} +#player-options .option-container .option-entry:hover { + background-color: rgba(20, 20, 20, 0.25); +} +#player-options .option-container .option-entry input[type=checkbox] { + margin-right: 0.25rem; +} +#player-options .option-container .option-entry input[type=number] { + max-width: 1.5rem; + max-height: 1rem; + margin-left: 0.125rem; + text-align: center; + /* Hide arrows on input[type=number] fields */ + -moz-appearance: textfield; +} +#player-options .option-container .option-entry input[type=number]::-webkit-outer-spin-button, #player-options .option-container .option-entry input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +#player-options .option-container .option-entry label { + flex-grow: 1; + margin-right: 0; + min-width: unset; + display: unset; +} +#player-options .randomize-button { + display: flex; + flex-direction: column; + justify-content: center; + height: 22px; + max-width: 30px; + margin: 0 0 0 0.25rem; + font-size: 14px; + border: 1px solid black; + border-radius: 3px; + background-color: #d3d3d3; + user-select: none; +} +#player-options .randomize-button:hover { + background-color: #c0c0c0; + cursor: pointer; +} +#player-options .randomize-button label { + line-height: 22px; + padding-left: 5px; + padding-right: 2px; + margin-right: 4px; + width: 100%; + height: 100%; + min-width: unset; +} +#player-options .randomize-button label:hover { + cursor: pointer; +} +#player-options .randomize-button input[type=checkbox] { + display: none; +} +#player-options .randomize-button:has(input[type=checkbox]:checked) { + background-color: #ffef00; /* Same as .interactive in globalStyles.css */ +} +#player-options .randomize-button:has(input[type=checkbox]:checked):hover { + background-color: #eedd27; +} +#player-options .randomize-button[data-tooltip]::after { + left: unset; + right: 0; +} +#player-options label { + display: block; + margin-right: 4px; + cursor: default; + word-break: break-word; +} +#player-options th, #player-options td { + border: none; + padding: 3px; + font-size: 17px; + vertical-align: top; +} + +@media all and (max-width: 1024px) { + #player-options { + border-radius: 0; + } + #player-options #meta-options { + flex-direction: column; + justify-content: flex-start; + gap: 6px; + } + #player-options .game-options { + justify-content: flex-start; + flex-wrap: wrap; + } +} + +/*# sourceMappingURL=playerOptions.css.map */ diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css.map b/WebHostLib/static/styles/playerOptions/playerOptions.css.map new file mode 100644 index 00000000..6797b88c --- /dev/null +++ b/WebHostLib/static/styles/playerOptions/playerOptions.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["playerOptions.scss"],"names":[],"mappings":"AAAQ;AAER;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;;AAIR;EACI;IACI;;EAEA;IACI;IACA;IACA;;EAGJ;IACI;IACA","file":"playerOptions.css"} \ No newline at end of file diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.scss b/WebHostLib/static/styles/playerOptions/playerOptions.scss new file mode 100644 index 00000000..525b8ef1 --- /dev/null +++ b/WebHostLib/static/styles/playerOptions/playerOptions.scss @@ -0,0 +1,364 @@ +@import "../markdown.css"; + +html{ + background-image: url('../../static/backgrounds/grass.png'); + background-repeat: repeat; + background-size: 650px 650px; + overflow-x: hidden; +} + +#player-options{ + box-sizing: border-box; + max-width: 1024px; + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: #eeffeb; + word-break: break-all; + + #player-options-header{ + h1{ + margin-bottom: 0; + padding-bottom: 0; + } + + h1:nth-child(2){ + font-size: 1.4rem; + margin-top: -8px; + margin-bottom: 0.5rem; + } + } + + .js-warning-banner{ + width: calc(100% - 1rem); + padding: 0.5rem; + border-radius: 4px; + background-color: #f3f309; + color: #000000; + margin-bottom: 0.5rem; + text-align: center; + } + + .group-container{ + padding: 0; + margin: 0; + + h2{ + user-select: none; + cursor: unset; + + label{ + cursor: pointer; + } + } + } + + #player-options-button-row{ + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; + } + + #user-message{ + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; + cursor: pointer; + } + + h1{ + font-size: 2.5rem; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + text-shadow: 1px 1px 4px #000000; + } + + h2{ + font-size: 40px; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + text-transform: lowercase; + text-shadow: 1px 1px 2px #000000; + } + + h3, h4, h5, h6{ + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + } + + input:not([type]){ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + + &:focus{ + border: 1px solid #ffffff; + } + } + + select{ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + background-color: #ffffff; + text-overflow: ellipsis; + } + + .game-options{ + display: flex; + flex-direction: row; + + .left, .right{ + display: grid; + grid-template-columns: 12rem auto; + grid-row-gap: 0.5rem; + grid-auto-rows: min-content; + align-items: start; + min-width: 480px; + width: 50%; + } + } + + #meta-options{ + display: flex; + justify-content: space-between; + gap: 20px; + padding: 3px; + + input, select{ + box-sizing: border-box; + width: 200px; + } + } + + .left, .right{ + flex-grow: 1; + margin-bottom: 0.5rem; + } + + .left{ + margin-right: 20px; + } + + .select-container{ + display: flex; + flex-direction: row; + max-width: 270px; + + select{ + min-width: 200px; + flex-grow: 1; + + &:disabled{ + background-color: lightgray; + } + } + } + + .range-container{ + display: flex; + flex-direction: row; + max-width: 270px; + + input[type=range]{ + flex-grow: 1; + } + + .range-value{ + min-width: 20px; + margin-left: 0.25rem; + } + } + + .named-range-container{ + display: flex; + flex-direction: column; + max-width: 270px; + + .named-range-wrapper{ + display: flex; + flex-direction: row; + margin-top: 0.25rem; + + input[type=range]{ + flex-grow: 1; + } + } + } + + .free-text-container{ + display: flex; + flex-direction: column; + max-width: 270px; + + input[type=text]{ + flex-grow: 1; + } + } + + .text-choice-container{ + display: flex; + flex-direction: column; + max-width: 270px; + + .text-choice-wrapper{ + display: flex; + flex-direction: row; + margin-bottom: 0.25rem; + + select{ + flex-grow: 1; + } + } + } + + .option-container{ + display: flex; + flex-direction: column; + background-color: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(20, 20, 20, 0.25); + border-radius: 3px; + color: #ffffff; + max-height: 10rem; + min-width: 14.5rem; + overflow-y: auto; + padding-right: 0.25rem; + padding-left: 0.25rem; + + .option-divider{ + width: 100%; + height: 2px; + background-color: rgba(20, 20, 20, 0.25); + margin-top: 0.125rem; + margin-bottom: 0.125rem; + } + + .option-entry{ + display: flex; + flex-direction: row; + align-items: flex-start; + margin-bottom: 0.125rem; + margin-top: 0.125rem; + user-select: none; + + &:hover{ + background-color: rgba(20, 20, 20, 0.25); + } + + input[type=checkbox]{ + margin-right: 0.25rem; + } + + input[type=number]{ + max-width: 1.5rem; + max-height: 1rem; + margin-left: 0.125rem; + text-align: center; + + /* Hide arrows on input[type=number] fields */ + -moz-appearance: textfield; + &::-webkit-outer-spin-button, &::-webkit-inner-spin-button{ + -webkit-appearance: none; + margin: 0; + } + } + + label{ + flex-grow: 1; + margin-right: 0; + min-width: unset; + display: unset; + } + } + } + + .randomize-button{ + display: flex; + flex-direction: column; + justify-content: center; + height: 22px; + max-width: 30px; + margin: 0 0 0 0.25rem; + font-size: 14px; + border: 1px solid black; + border-radius: 3px; + background-color: #d3d3d3; + user-select: none; + + &:hover{ + background-color: #c0c0c0; + cursor: pointer; + } + + label{ + line-height: 22px; + padding-left: 5px; + padding-right: 2px; + margin-right: 4px; + width: 100%; + height: 100%; + min-width: unset; + &:hover{ + cursor: pointer; + } + } + + input[type=checkbox]{ + display: none; + } + + &:has(input[type=checkbox]:checked){ + background-color: #ffef00; /* Same as .interactive in globalStyles.css */ + + &:hover{ + background-color: #eedd27; + } + } + + &[data-tooltip]::after{ + left: unset; + right: 0; + } + } + + label{ + display: block; + margin-right: 4px; + cursor: default; + word-break: break-word; + } + + th, td{ + border: none; + padding: 3px; + font-size: 17px; + vertical-align: top; + } +} + +@media all and (max-width: 1024px) { + #player-options { + border-radius: 0; + + #meta-options { + flex-direction: column; + justify-content: flex-start; + gap: 6px; + } + + .game-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + } +} diff --git a/WebHostLib/static/styles/supportedGames.css b/WebHostLib/static/styles/supportedGames.css index 7396daa9..ab12f320 100644 --- a/WebHostLib/static/styles/supportedGames.css +++ b/WebHostLib/static/styles/supportedGames.css @@ -8,30 +8,15 @@ cursor: unset; } -#games h1{ +#games h1, #games details summary.h1{ font-size: 60px; cursor: unset; } -#games h2{ +#games h2, #games details summary.h2{ color: #93dcff; margin-bottom: 2px; -} - -#games .collapse-toggle{ - cursor: pointer; -} - -#games h2 .collapse-arrow{ - font-size: 20px; - display: inline-block; /* make vertical-align work */ - padding-bottom: 9px; - vertical-align: middle; - padding-right: 8px; -} - -#games p.collapsed{ - display: none; + text-transform: none; } #games a{ diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 7cd8463f..02992b18 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -42,6 +42,7 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, [data-tooltip]:hover:before, [data-tooltip]:hover:after, .tooltip:hover:before, .tooltip:hover:after{ visibility: visible; opacity: 1; + word-break: break-word; } /** Directional arrow styles */ diff --git a/WebHostLib/static/styles/weighted-options.css b/WebHostLib/static/styles/weighted-options.css deleted file mode 100644 index 8a66ca23..00000000 --- a/WebHostLib/static/styles/weighted-options.css +++ /dev/null @@ -1,315 +0,0 @@ -html{ - background-image: url('../static/backgrounds/grass.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{ - 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 th, #weighted-settings table td{ - border: none; -} - -#weighted-settings table td{ - padding: 5px; -} - -#weighted-settings table .td-left{ - font-family: LexendDeca-Regular, sans-serif; - 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 .items-wrapper{ - display: flex; - flex-direction: row; - justify-content: space-between; -} - -#weighted-settings .items-div h3{ - margin-bottom: 0.5rem; -} - -#weighted-settings .items-wrapper .item-set-wrapper{ - width: 24%; - font-weight: bold; -} - -#weighted-settings .item-container{ - border: 1px solid #ffffff; - border-radius: 2px; - width: 100%; - height: 300px; - overflow-y: auto; - overflow-x: hidden; - margin-top: 0.125rem; - font-weight: normal; -} - -#weighted-settings .item-container .item-div{ - padding: 0.125rem 0.5rem; - cursor: pointer; -} - -#weighted-settings .item-container .item-div:hover{ - 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, #weighted-settings .locations-div{ - margin-top: 2rem; -} - -#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{ - margin-bottom: 0.5rem; -} - -#weighted-settings .hints-container, #weighted-settings .locations-container{ - display: flex; - flex-direction: row; - justify-content: space-between; -} - -#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{ - width: calc(50% - 0.5rem); - font-weight: bold; -} - -#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{ - margin-top: 0.25rem; - height: 300px; - font-weight: normal; -} - -#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; - cursor: pointer; -} - -#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: none; - 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); - text-transform: none; -} - -#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 .simple-list{ - display: flex; - flex-direction: column; - - max-height: 300px; - overflow-y: auto; - border: 1px solid #ffffff; - border-radius: 4px; -} - -#weighted-settings .simple-list .list-row label{ - display: block; - width: calc(100% - 0.5rem); - padding: 0.0625rem 0.25rem; -} - -#weighted-settings .simple-list .list-row label:hover{ - background-color: rgba(0, 0, 0, 0.1); -} - -#weighted-settings .simple-list .list-row label input[type=checkbox]{ - margin-right: 0.5rem; -} - -#weighted-settings .simple-list hr{ - width: calc(100% - 2px); - margin: 2px auto; - border-bottom: 1px solid rgb(255 255 255 / 0.6); -} - -#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; - } -} diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css b/WebHostLib/static/styles/weightedOptions/weightedOptions.css new file mode 100644 index 00000000..3cfc6d24 --- /dev/null +++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css @@ -0,0 +1,232 @@ +html { + background-image: url("../../static/backgrounds/grass.png"); + background-repeat: repeat; + background-size: 650px 650px; + scroll-padding-top: 90px; +} + +#weighted-options { + 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-options #weighted-options-header h1 { + margin-bottom: 0; + padding-bottom: 0; +} +#weighted-options #weighted-options-header h1:nth-child(2) { + font-size: 1.4rem; + margin-top: -8px; + margin-bottom: 0.5rem; +} +#weighted-options .js-warning-banner { + width: calc(100% - 1rem); + padding: 0.5rem; + border-radius: 4px; + background-color: #f3f309; + color: #000000; + margin-bottom: 0.5rem; + text-align: center; +} +#weighted-options .option-wrapper { + width: 100%; + margin-bottom: 2rem; +} +#weighted-options .option-wrapper .add-option-div { + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-bottom: 1rem; +} +#weighted-options .option-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-options .option-wrapper .add-option-div button:active { + margin-bottom: 1px; +} +#weighted-options p.option-description { + margin: 0 0 1rem; +} +#weighted-options p.hint-text { + margin: 0 0 1rem; + font-style: italic; +} +#weighted-options table { + width: 100%; + margin-top: 0.5rem; + margin-bottom: 1.5rem; +} +#weighted-options table th, #weighted-options table td { + border: none; +} +#weighted-options table td { + padding: 5px; +} +#weighted-options table .td-left { + font-family: LexendDeca-Regular, sans-serif; + padding-right: 1rem; + width: 200px; +} +#weighted-options table .td-middle { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-right: 1rem; +} +#weighted-options table .td-right { + width: 4rem; + text-align: right; +} +#weighted-options table .td-delete { + width: 50px; + text-align: right; +} +#weighted-options table .range-option-delete { + cursor: pointer; +} +#weighted-options #weighted-options-button-row { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; +} +#weighted-options #user-message { + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; +} +#weighted-options #user-message.visible { + display: block; + cursor: pointer; +} +#weighted-options h1 { + font-size: 2.5rem; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + color: #ffffff; + text-shadow: 1px 1px 4px #000000; +} +#weighted-options h2, #weighted-options details summary.h2 { + font-size: 2rem; + font-weight: normal; + border-bottom: 1px solid #ffffff; + width: 100%; + margin-bottom: 0.5rem; + color: #ffe993; + text-transform: none; + text-shadow: 1px 1px 2px #000000; +} +#weighted-options h3, #weighted-options h4, #weighted-options h5, #weighted-options h6 { + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + text-transform: none; + cursor: unset; +} +#weighted-options h3.option-group-header { + margin-top: 0.75rem; + font-weight: bold; +} +#weighted-options a { + color: #ffef00; + cursor: pointer; +} +#weighted-options input:not([type]) { + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; +} +#weighted-options input:not([type]):focus { + border: 1px solid #ffffff; +} +#weighted-options .invisible { + display: none; +} +#weighted-options .unsupported-option { + margin-top: 0.5rem; +} +#weighted-options .set-container, #weighted-options .dict-container, #weighted-options .list-container { + display: flex; + flex-direction: column; + background-color: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(20, 20, 20, 0.25); + border-radius: 3px; + color: #ffffff; + max-height: 15rem; + min-width: 14.5rem; + overflow-y: auto; + padding-right: 0.25rem; + padding-left: 0.25rem; + margin-top: 0.5rem; +} +#weighted-options .set-container .divider, #weighted-options .dict-container .divider, #weighted-options .list-container .divider { + width: 100%; + height: 2px; + background-color: rgba(20, 20, 20, 0.25); + margin-top: 0.125rem; + margin-bottom: 0.125rem; +} +#weighted-options .set-container .set-entry, #weighted-options .set-container .dict-entry, #weighted-options .set-container .list-entry, #weighted-options .dict-container .set-entry, #weighted-options .dict-container .dict-entry, #weighted-options .dict-container .list-entry, #weighted-options .list-container .set-entry, #weighted-options .list-container .dict-entry, #weighted-options .list-container .list-entry { + display: flex; + flex-direction: row; + align-items: flex-start; + padding-bottom: 0.25rem; + padding-top: 0.25rem; + user-select: none; + line-height: 1rem; +} +#weighted-options .set-container .set-entry:hover, #weighted-options .set-container .dict-entry:hover, #weighted-options .set-container .list-entry:hover, #weighted-options .dict-container .set-entry:hover, #weighted-options .dict-container .dict-entry:hover, #weighted-options .dict-container .list-entry:hover, #weighted-options .list-container .set-entry:hover, #weighted-options .list-container .dict-entry:hover, #weighted-options .list-container .list-entry:hover { + background-color: rgba(20, 20, 20, 0.25); +} +#weighted-options .set-container .set-entry input[type=checkbox], #weighted-options .set-container .dict-entry input[type=checkbox], #weighted-options .set-container .list-entry input[type=checkbox], #weighted-options .dict-container .set-entry input[type=checkbox], #weighted-options .dict-container .dict-entry input[type=checkbox], #weighted-options .dict-container .list-entry input[type=checkbox], #weighted-options .list-container .set-entry input[type=checkbox], #weighted-options .list-container .dict-entry input[type=checkbox], #weighted-options .list-container .list-entry input[type=checkbox] { + margin-right: 0.25rem; +} +#weighted-options .set-container .set-entry input[type=number], #weighted-options .set-container .dict-entry input[type=number], #weighted-options .set-container .list-entry input[type=number], #weighted-options .dict-container .set-entry input[type=number], #weighted-options .dict-container .dict-entry input[type=number], #weighted-options .dict-container .list-entry input[type=number], #weighted-options .list-container .set-entry input[type=number], #weighted-options .list-container .dict-entry input[type=number], #weighted-options .list-container .list-entry input[type=number] { + max-width: 1.5rem; + max-height: 1rem; + margin-left: 0.125rem; + text-align: center; + /* Hide arrows on input[type=number] fields */ + -moz-appearance: textfield; +} +#weighted-options .set-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .set-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .dict-container .list-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .set-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .dict-entry input[type=number]::-webkit-inner-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-outer-spin-button, #weighted-options .list-container .list-entry input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +#weighted-options .set-container .set-entry label, #weighted-options .set-container .dict-entry label, #weighted-options .set-container .list-entry label, #weighted-options .dict-container .set-entry label, #weighted-options .dict-container .dict-entry label, #weighted-options .dict-container .list-entry label, #weighted-options .list-container .set-entry label, #weighted-options .list-container .dict-entry label, #weighted-options .list-container .list-entry label { + flex-grow: 1; + margin-right: 0; + min-width: unset; + display: unset; +} + +.hidden { + display: none; +} + +@media all and (max-width: 1000px), all and (orientation: portrait) { + #weighted-options .game-options { + justify-content: flex-start; + flex-wrap: wrap; + } + #game-options table label { + display: block; + min-width: 200px; + } +} + +/*# sourceMappingURL=weightedOptions.css.map */ diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map new file mode 100644 index 00000000..7c57cde0 --- /dev/null +++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAOZ;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;EACA;AAEA;EACA;;AACA;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;;AAMhB;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"} \ No newline at end of file diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.scss b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss new file mode 100644 index 00000000..7ff3a2c3 --- /dev/null +++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss @@ -0,0 +1,274 @@ +html{ + background-image: url('../../static/backgrounds/grass.png'); + background-repeat: repeat; + background-size: 650px 650px; + scroll-padding-top: 90px; +} + +#weighted-options{ + 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-options-header{ + h1{ + margin-bottom: 0; + padding-bottom: 0; + } + + h1:nth-child(2){ + font-size: 1.4rem; + margin-top: -8px; + margin-bottom: 0.5rem; + } + } + + .js-warning-banner{ + width: calc(100% - 1rem); + padding: 0.5rem; + border-radius: 4px; + background-color: #f3f309; + color: #000000; + margin-bottom: 0.5rem; + text-align: center; + } + + .option-wrapper{ + width: 100%; + margin-bottom: 2rem; + + .add-option-div{ + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-bottom: 1rem; + + button{ + width: auto; + height: auto; + margin: 0 0 0 0.15rem; + padding: 0 0.25rem; + border-radius: 4px; + cursor: default; + + &:active{ + margin-bottom: 1px; + } + } + } + } + + p{ + &.option-description{ + margin: 0 0 1rem; + } + + &.hint-text{ + margin: 0 0 1rem; + font-style: italic; + }; + } + + table{ + width: 100%; + margin-top: 0.5rem; + margin-bottom: 1.5rem; + + th, td{ + border: none; + } + + td{ + padding: 5px; + } + + .td-left{ + font-family: LexendDeca-Regular, sans-serif; + padding-right: 1rem; + width: 200px; + } + + .td-middle{ + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-right: 1rem; + } + + .td-right{ + width: 4rem; + text-align: right; + } + + .td-delete{ + width: 50px; + text-align: right; + } + + .range-option-delete{ + cursor: pointer; + } + } + + #weighted-options-button-row{ + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 15px; + } + + #user-message{ + display: none; + width: calc(100% - 8px); + background-color: #ffe86b; + border-radius: 4px; + color: #000000; + padding: 4px; + text-align: center; + + &.visible{ + display: block; + cursor: pointer; + } + } + + h1{ + font-size: 2.5rem; + font-weight: normal; + width: 100%; + margin-bottom: 0.5rem; + color: #ffffff; + text-shadow: 1px 1px 4px #000000; + } + + h2, details summary.h2{ + font-size: 2rem; + font-weight: normal; + border-bottom: 1px solid #ffffff; + width: 100%; + margin-bottom: 0.5rem; + color: #ffe993; + text-transform: none; + text-shadow: 1px 1px 2px #000000; + } + + h3, h4, h5, h6{ + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + text-transform: none; + cursor: unset; + } + + h3{ + &.option-group-header{ + margin-top: 0.75rem; + font-weight: bold; + } + } + + a{ + color: #ffef00; + cursor: pointer; + } + + input:not([type]){ + border: 1px solid #000000; + padding: 3px; + border-radius: 3px; + min-width: 150px; + + &:focus{ + border: 1px solid #ffffff; + } + } + + .invisible{ + display: none; + } + + .unsupported-option{ + margin-top: 0.5rem; + } + + .set-container, .dict-container, .list-container{ + display: flex; + flex-direction: column; + background-color: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(20, 20, 20, 0.25); + border-radius: 3px; + color: #ffffff; + max-height: 15rem; + min-width: 14.5rem; + overflow-y: auto; + padding-right: 0.25rem; + padding-left: 0.25rem; + margin-top: 0.5rem; + + .divider{ + width: 100%; + height: 2px; + background-color: rgba(20, 20, 20, 0.25); + margin-top: 0.125rem; + margin-bottom: 0.125rem; + } + + .set-entry, .dict-entry, .list-entry{ + display: flex; + flex-direction: row; + align-items: flex-start; + padding-bottom: 0.25rem; + padding-top: 0.25rem; + user-select: none; + line-height: 1rem; + + &:hover{ + background-color: rgba(20, 20, 20, 0.25); + } + + input[type=checkbox]{ + margin-right: 0.25rem; + } + + input[type=number]{ + max-width: 1.5rem; + max-height: 1rem; + margin-left: 0.125rem; + text-align: center; + + /* Hide arrows on input[type=number] fields */ + -moz-appearance: textfield; + &::-webkit-outer-spin-button, &::-webkit-inner-spin-button{ + -webkit-appearance: none; + margin: 0; + } + } + + label{ + flex-grow: 1; + margin-right: 0; + min-width: unset; + display: unset; + } + } + } +} + +.hidden{ + display: none; +} + +@media all and (max-width: 1000px), all and (orientation: portrait){ + #weighted-options .game-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + + #game-options table label{ + display: block; + min-width: 200px; + } +} diff --git a/WebHostLib/templates/player-options.html b/WebHostLib/templates/player-options.html deleted file mode 100644 index 4c749752..00000000 --- a/WebHostLib/templates/player-options.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {{ game }} Options - - - - - - -{% endblock %} - -{% block body %} - {% include 'header/'+theme+'Header.html' %} -
-
-

Player Options

-

Choose the options you would like to play with! You may generate a single-player game from this page, - or download an options file you can use to participate in a MultiWorld.

- -

- A more advanced options configuration for all games can be found on the - Weighted options page. -
- A list of all games you have generated can be found on the User Content Page. -
- You may also download the - template file for this game. -

- -
-
- - -
-
- - -
- -
- -

Game Options

-
-
-
-
- -
- - - -
-
-{% endblock %} diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html new file mode 100644 index 00000000..64964682 --- /dev/null +++ b/WebHostLib/templates/playerOptions/macros.html @@ -0,0 +1,210 @@ +{% macro Toggle(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ + {{ RandomizeButton(option_name, option) }} +
+{% endmacro %} + +{% macro Choice(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ + {{ RandomizeButton(option_name, option) }} +
+{% endmacro %} + +{% macro Range(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ + + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} + + {{ RandomizeButton(option_name, option) }} +
+{% endmacro %} + +{% macro NamedRange(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ +
+ + + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} + + {{ RandomizeButton(option_name, option) }} +
+
+{% endmacro %} + +{% macro FreeText(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ +
+{% endmacro %} + +{% macro TextChoice(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+
+ + {{ RandomizeButton(option_name, option) }} +
+ +
+{% endmacro %} + +{% macro ItemDict(option_name, option, world) %} + {{ OptionTitle(option_name, option) }} +
+ {% for item_name in world.item_names|sort %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro OptionList(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro LocationSet(option_name, option, world) %} + {{ OptionTitle(option_name, option) }} +
+ {% for group_name in world.location_name_groups.keys()|sort %} + {% if group_name != "Everywhere" %} +
+ + +
+ {% endif %} + {% endfor %} + {% if world.location_name_groups.keys()|length > 1 %} +
 
+ {% endif %} + {% for location_name in world.location_names|sort %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro ItemSet(option_name, option, world) %} + {{ OptionTitle(option_name, option) }} +
+ {% for group_name in world.item_name_groups.keys()|sort %} + {% if group_name != "Everything" %} +
+ + +
+ {% endif %} + {% endfor %} + {% if world.item_name_groups.keys()|length > 1 %} +
 
+ {% endif %} + {% for item_name in world.item_names|sort %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro OptionSet(option_name, option) %} + {{ OptionTitle(option_name, option) }} +
+ {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %} +
+ + +
+ {% endfor %} +
+{% endmacro %} + +{% macro OptionTitle(option_name, option) %} + +{% endmacro %} + +{% macro RandomizeButton(option_name, option) %} +
+ +
+{% endmacro %} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html new file mode 100644 index 00000000..56576109 --- /dev/null +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -0,0 +1,166 @@ +{% extends 'pageWrapper.html' %} +{% import 'playerOptions/macros.html' as inputs %} + +{% block head %} + {{ world_name }} Options + + + + + + +{% endblock %} + +{% block body %} + {% include 'header/'+theme+'Header.html' %} +
+ + +
{{ message }}
+ +
+

{{ world_name }}

+

Player Options

+
+

Choose the options you would like to play with! You may generate a single-player game from this page, + or download an options file you can use to participate in a MultiWorld.

+ +

+ A more advanced options configuration for all games can be found on the + Weighted options page. +
+ A list of all games you have generated can be found on the User Content Page. +
+ You may also download the + template file for this game. +

+ +
+
+
+ + +
+
+ + +
+
+ +
+ {% for group_name, group_options in option_groups.items() %} +
+ {{ group_name }} +
+
+ {% for option_name, option in group_options.items() %} + {% if loop.index <= (loop.length / 2)|round(0,"ceil") %} + {% if issubclass(option, Options.Toggle) %} + {{ inputs.Toggle(option_name, option) }} + + {% elif issubclass(option, Options.TextChoice) %} + {{ inputs.TextChoice(option_name, option) }} + + {% elif issubclass(option, Options.Choice) %} + {{ inputs.Choice(option_name, option) }} + + {% elif issubclass(option, Options.NamedRange) %} + {{ inputs.NamedRange(option_name, option) }} + + {% elif issubclass(option, Options.Range) %} + {{ inputs.Range(option_name, option) }} + + {% elif issubclass(option, Options.FreeText) %} + {{ inputs.FreeText(option_name, option) }} + + {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} + {{ inputs.ItemDict(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionList) and option.valid_keys %} + {{ inputs.OptionList(option_name, option) }} + + {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} + {{ inputs.LocationSet(option_name, option, world) }} + + {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} + {{ inputs.ItemSet(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} + {{ inputs.OptionSet(option_name, option) }} + + {% endif %} + {% endif %} + {% endfor %} +
+
+ {% for option_name, option in group_options.items() %} + {% if loop.index > (loop.length / 2)|round(0,"ceil") %} + {% if issubclass(option, Options.Toggle) %} + {{ inputs.Toggle(option_name, option) }} + + {% elif issubclass(option, Options.TextChoice) %} + {{ inputs.TextChoice(option_name, option) }} + + {% elif issubclass(option, Options.Choice) %} + {{ inputs.Choice(option_name, option) }} + + {% elif issubclass(option, Options.NamedRange) %} + {{ inputs.NamedRange(option_name, option) }} + + {% elif issubclass(option, Options.Range) %} + {{ inputs.Range(option_name, option) }} + + {% elif issubclass(option, Options.FreeText) %} + {{ inputs.FreeText(option_name, option) }} + + {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} + {{ inputs.ItemDict(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionList) and option.valid_keys %} + {{ inputs.OptionList(option_name, option) }} + + {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} + {{ inputs.LocationSet(option_name, option, world) }} + + {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} + {{ inputs.ItemSet(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} + {{ inputs.OptionSet(option_name, option) }} + + {% endif %} + {% endif %} + {% endfor %} +
+
+
+ {% endfor %} +
+ +
+ + +
+
+
+{% endblock %} diff --git a/WebHostLib/templates/siteMap.html b/WebHostLib/templates/siteMap.html index 231ec83e..cdd6ad45 100644 --- a/WebHostLib/templates/siteMap.html +++ b/WebHostLib/templates/siteMap.html @@ -24,7 +24,6 @@
  • Supported Games Page
  • Tutorials Page
  • User Content
  • -
  • Weighted Options Page
  • Game Statistics
  • Glossary
  • @@ -50,8 +49,12 @@ diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 393edcfe..b3f20d29 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -41,10 +41,8 @@ {% for game_name in worlds | title_sorted %} {% set world = worlds[game_name] %} -

    - {{ game_name }} -

    -
    + {{ game_name }} {{ world.__doc__ | default("No description provided.", true) }}
    Game Page {% if world.web.tutorials %} @@ -53,16 +51,18 @@ {% endif %} {% if world.web.options_page is string %} | - Options Page + Options Page (External Link) {% elif world.web.options_page %} | Options Page + | + Advanced Options {% endif %} {% if world.web.bug_report_page %} | Report a Bug {% endif %} -

    +
    {% endfor %} {% endblock %} diff --git a/WebHostLib/templates/weighted-options.html b/WebHostLib/templates/weighted-options.html deleted file mode 100644 index 032a4eeb..00000000 --- a/WebHostLib/templates/weighted-options.html +++ /dev/null @@ -1,48 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {{ game }} Options - - - - - - -{% endblock %} - -{% block body %} - {% include 'header/grassHeader.html' %} -
    -
    -

    Weighted Options

    -

    Weighted options allow 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.

    - -

    Choose the games and options you would like to play with! You may generate a single-player game from - this page, or download an options file you can use to participate in a MultiWorld.

    - -

    A list of all games you have generated can be found on the User Content - page.

    - -


    - -

    - -
    - -
    - - -
    - -
    - -
    - - - -
    -
    -{% endblock %} diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html new file mode 100644 index 00000000..e7caab93 --- /dev/null +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -0,0 +1,249 @@ +{% macro Toggle(option_name, option) %} + + + {{ RangeRow(option_name, option, "No", "false") }} + {{ RangeRow(option_name, option, "Yes", "true") }} + {{ RandomRows(option_name, option) }} + +
    +{% endmacro %} + +{% macro DefaultOnToggle(option_name, option) %} + + {{ Toggle(option_name, option) }} +{% endmacro %} + +{% macro Choice(option_name, option) %} + + + {% for id, name in option.name_lookup.items() %} + {% if name != 'random' %} + {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {% endif %} + {% endfor %} + {{ RandomRows(option_name, option) }} + +
    +{% endmacro %} + +{% macro Range(option_name, option) %} +
    + This is a range option. +

    + Accepted values:
    + Normal range: {{ option.range_start }} - {{ option.range_end }} + {% if option.special_range_names %} +

    + The following values has special meaning, and may fall outside the normal range. +
      + {% for name, value in option.special_range_names.items() %} +
    • {{ value }}: {{ name }}
    • + {% endfor %} +
    + {% endif %} +
    + + +
    +
    + + + {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} + {% if option.range_start < option.default < option.range_end %} + {{ RangeRow(option_name, option, option.default, option.default, True) }} + {% endif %} + {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }} + {{ RandomRows(option_name, option) }} + +
    +{% endmacro %} + +{% macro NamedRange(option_name, option) %} + + {{ Range(option_name, option) }} +{% endmacro %} + +{% macro FreeText(option_name, option) %} +
    + This option allows custom values only. Please enter your desired values below. +
    + + +
    + + + + +
    +
    +{% endmacro %} + +{% macro TextChoice(option_name, option) %} +
    + Custom values are also allowed for this option. To create one, enter it into the input box below. +
    + + +
    +
    + + + {% for id, name in option.name_lookup.items() %} + {% if name != 'random' %} + {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {% endif %} + {% endfor %} + {{ RandomRows(option_name, option) }} + +
    +{% endmacro %} + +{% macro PlandoBosses(option_name, option) %} + + {{ TextChoice(option_name, option) }} +{% endmacro %} + +{% macro ItemDict(option_name, option, world) %} +
    + {% for item_name in world.item_names|sort %} +
    + + +
    + {% endfor %} +
    +{% endmacro %} + +{% macro OptionList(option_name, option) %} +
    + {% for key in option.valid_keys|sort %} +
    + + +
    + {% endfor %} +
    +{% endmacro %} + +{% macro LocationSet(option_name, option, world) %} +
    + {% for group_name in world.location_name_groups.keys()|sort %} + {% if group_name != "Everywhere" %} +
    + + +
    + {% endif %} + {% endfor %} + {% if world.location_name_groups.keys()|length > 1 %} +
     
    + {% endif %} + {% for location_name in world.location_names|sort %} +
    + + +
    + {% endfor %} +
    +{% endmacro %} + +{% macro ItemSet(option_name, option, world) %} +
    + {% for group_name in world.item_name_groups.keys()|sort %} + {% if group_name != "Everything" %} +
    + + +
    + {% endif %} + {% endfor %} + {% if world.item_name_groups.keys()|length > 1 %} +
     
    + {% endif %} + {% for item_name in world.item_names|sort %} +
    + + +
    + {% endfor %} +
    +{% endmacro %} + +{% macro OptionSet(option_name, option) %} +
    + {% for key in option.valid_keys|sort %} +
    + + +
    + {% endfor %} +
    +{% endmacro %} + +{% macro OptionTitleTd(option_name, value) %} + + + +{% endmacro %} + +{% macro RandomRows(option_name, option, extra_column=False) %} + {% for key, value in {"Random": "random", "Random (Low)": "random-low", "Random (Middle)": "random-middle", "Random (High)": "random-high"}.items() %} + {{ RangeRow(option_name, option, key, value) }} + {% endfor %} +{% endmacro %} + +{% macro RangeRow(option_name, option, display_value, value, can_delete=False) %} + + + + + + + + + + {% if option.default == value %} + 25 + {% else %} + 0 + {% endif %} + + + {% if can_delete %} + + + ❌ + + + {% else %} + + {% endif %} + +{% endmacro %} diff --git a/WebHostLib/templates/weightedOptions/weightedOptions.html b/WebHostLib/templates/weightedOptions/weightedOptions.html new file mode 100644 index 00000000..c21671a8 --- /dev/null +++ b/WebHostLib/templates/weightedOptions/weightedOptions.html @@ -0,0 +1,119 @@ +{% extends 'pageWrapper.html' %} +{% import 'weightedOptions/macros.html' as inputs %} + +{% block head %} + {{ world_name }} Weighted Options + + + + +{% endblock %} + +{% block body %} + {% include 'header/'+theme+'Header.html' %} +
    + + +
    + +
    +

    {{ world_name }}

    +

    Weighted Options

    +
    + +
    + +

    Weighted options allow you to choose how likely a particular option's value is to be used in game + generation. The higher a value is weighted, the more likely the option will be chosen. Think of them like + entries in a raffle.

    + +

    Choose the options you would like to play with! You may generate a single-player game from + this page, or download an options file you can use to participate in a MultiWorld.

    + +

    A list of all games you have generated can be found on the User Content + page.

    + + +


    + +

    + +
    + {% for group_name, group_options in option_groups.items() %} +
    + {{ group_name }} + {% for option_name, option in group_options.items() %} +
    +

    {{ option.display_name|default(option_name) }}

    +
    + {{ option.__doc__ }} +
    + {% if issubclass(option, Options.Toggle) %} + {{ inputs.Toggle(option_name, option) }} + + {% elif issubclass(option, Options.DefaultOnToggle) %} + {{ inputs.DefaultOnToggle(option_name, option) }} + + {% elif issubclass(option, Options.PlandoBosses) %} + {{ inputs.PlandoBosses(option_name, option) }} + + {% elif issubclass(option, Options.TextChoice) %} + {{ inputs.TextChoice(option_name, option) }} + + {% elif issubclass(option, Options.Choice) %} + {{ inputs.Choice(option_name, option) }} + + {% elif issubclass(option, Options.NamedRange) %} + {{ inputs.NamedRange(option_name, option) }} + + {% elif issubclass(option, Options.Range) %} + {{ inputs.Range(option_name, option) }} + + {% elif issubclass(option, Options.FreeText) %} + {{ inputs.FreeText(option_name, option) }} + + {% elif issubclass(option, Options.ItemDict) and option.verify_item_name %} + {{ inputs.ItemDict(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionList) and option.valid_keys %} + {{ inputs.OptionList(option_name, option) }} + + {% elif issubclass(option, Options.LocationSet) and option.verify_location_name %} + {{ inputs.LocationSet(option_name, option, world) }} + + {% elif issubclass(option, Options.ItemSet) and option.verify_item_name %} + {{ inputs.ItemSet(option_name, option, world) }} + + {% elif issubclass(option, Options.OptionSet) and option.valid_keys %} + {{ inputs.OptionSet(option_name, option) }} + + {% else %} +
    + This option is not supported. Please edit your .yaml file manually. +
    + + {% endif %} +
    + {% endfor %} +
    + {% endfor %} +
    + +
    + + +
    +
    +
    +{% endblock %} diff --git a/data/options.yaml b/data/options.yaml index 30bd328f..8eea75a7 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -45,7 +45,10 @@ requires: {% endmacro %} {{ game }}: - {%- for option_key, option in options.items() %} + {%- for group_name, group_options in option_groups.items() %} + # {{ group_name }} + + {%- for option_key, option in group_options.items() %} {{ option_key }}: {%- if option.__doc__ %} # {{ option.__doc__ @@ -83,3 +86,4 @@ requires: {%- endif -%} {{ "\n" }} {%- endfor %} + {%- endfor %} diff --git a/docs/options api.md b/docs/options api.md index bade33c0..dbf37df7 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -85,6 +85,25 @@ class ExampleWorld(World): options: ExampleGameOptions ``` +### Option Groups +Options may be categorized into groups for display on the WebHost. Option groups are displayed alphabetically on the +player-options and weighted-options pages. Options without a group name are categorized into a generic "Game Options" +group. + +```python +from worlds.AutoWorld import WebWorld +from BaseClasses import OptionGroup + +class MyWorldWeb(WebWorld): + option_groups = [ + OptionGroup('Color Options', [ + Options.ColorblindMode, + Options.FlashReduction, + Options.UIColors, + ]), + ] +``` + ### Option Checking Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after world instantiation. These are created as attributes on the MultiWorld and can be accessed with diff --git a/docs/world api.md b/docs/world api.md index 4f9fc2b1..6714fa3a 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -181,8 +181,7 @@ required, and will prevent progression and useful items from being placed at exc #### Documenting Locations Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and -location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra -indentation and single newlines will be collapsed into spaces. +location groups. These descriptions will show up in location-selection options on the options pages. ```python # locations.py @@ -236,8 +235,7 @@ Other classifications include: #### Documenting Items Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item -groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and -single newlines will be collapsed into spaces. +groups. These descriptions will show up in item-selection options on the options pages. ```python # items.py diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f77c1661..d269ee10 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,7 +3,7 @@ from __future__ import annotations import hashlib import logging import pathlib -import random +from random import Random import re import sys import time @@ -11,11 +11,13 @@ from dataclasses import make_dataclass from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) -from Options import PerGameCommonOptions -from BaseClasses import CollectionState +from Options import ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, PerGameCommonOptions, \ + PriorityLocations, \ + StartHints, \ + StartInventory, StartInventoryPool, StartLocationHints +from BaseClasses import CollectionState, OptionGroup if TYPE_CHECKING: - import random from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance from . import GamesPackage from settings import Group @@ -118,6 +120,33 @@ class AutoLogicRegister(type): return new_class +class WebWorldRegister(type): + def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> WebWorldRegister: + # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the + # dev, putting it at the end if they don't define options in it + option_groups: List[OptionGroup] = dct.get("option_groups", []) + item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, + StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] + seen_options = [] + item_group_in_list = False + for group in option_groups: + assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined." + if group.name == "Item & Location Options": + group.options.extend(item_and_loc_options) + item_group_in_list = True + else: + for option in group.options: + assert option not in item_and_loc_options, \ + f"{option} cannot be moved out of the \"Item & Location Options\" Group" + assert len(group.options) == len(set(group.options)), f"Duplicate options in option group {group.name}" + for option in group.options: + assert option not in seen_options, f"{option} found in two option groups" + seen_options.append(option) + if not item_group_in_list: + option_groups.append(OptionGroup("Item & Location Options", item_and_loc_options)) + return super().__new__(mcs, name, bases, dct) + + def _timed_call(method: Callable[..., Any], *args: Any, multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any: start = time.perf_counter() @@ -172,7 +201,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: _timed_call(stage_callable, multiworld, *args) -class WebWorld: +class WebWorld(metaclass=WebWorldRegister): """Webhost integration""" options_page: Union[bool, str] = True @@ -194,6 +223,9 @@ class WebWorld: options_presets: Dict[str, Dict[str, Any]] = {} """A dictionary containing a collection of developer-defined game option presets.""" + option_groups: ClassVar[List[OptionGroup]] = [] + """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. @@ -283,7 +315,7 @@ class World(metaclass=AutoWorldRegister): location_names: ClassVar[Set[str]] """set of all potential location names""" - random: random.Random + random: Random """This world's random object. Should be used for any randomization needed in world for this player slot.""" settings_key: ClassVar[str] @@ -300,7 +332,7 @@ class World(metaclass=AutoWorldRegister): assert multiworld is not None self.multiworld = multiworld self.player = player - self.random = random.Random(multiworld.random.getrandbits(64)) + self.random = Random(multiworld.random.getrandbits(64)) multiworld.per_slot_randoms[player] = self.random def __getattr__(self, item: str) -> Any: diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py index fb09e532..9e0cc9d6 100644 --- a/worlds/adventure/Options.py +++ b/worlds/adventure/Options.py @@ -241,4 +241,4 @@ adventure_option_definitions: Dict[str, type(Option)] = { "difficulty_switch_b": DifficultySwitchB, "start_castle": StartCastle, -} \ No newline at end of file +} diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md index 0c907b1f..a8ed11cd 100644 --- a/worlds/alttp/docs/multiworld_es.md +++ b/worlds/alttp/docs/multiworld_es.md @@ -64,7 +64,8 @@ configuración personal y descargar un fichero "YAML". ### Configuración YAML avanzada -Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings), +Una version mas avanzada del fichero Yaml puede ser creada usando la pagina +["Weighted settings"](/games/A Link to the Past/weighted-options), la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser elegidos sobre otros de la misma. diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md index f2d55787..310f3a4f 100644 --- a/worlds/alttp/docs/multiworld_fr.md +++ b/worlds/alttp/docs/multiworld_fr.md @@ -66,9 +66,10 @@ paramètres personnels et de les exporter vers un fichier YAML. ### Configuration avancée du fichier YAML Une version plus avancée du fichier YAML peut être créée en utilisant la page -des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page -a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir -quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie. +des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à +trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs +glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux +autres disponibles dans une même catégorie. Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40. diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 045e48e2..5b1b583e 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -3,13 +3,12 @@ This guide covers more the more advanced options available in YAML files. This g to edit their YAML file manually. This guide should take about 10 minutes to read. If you would like to generate a basic, fully playable YAML without editing a file, then visit the options page for the -game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here. +game you intend to play. The options page can be found on the supported games page, just click the "Options Page" link under the name of the game you would like. * Supported games page: [Archipelago Games List](/games) -* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings) Clicking on the "Export Options" button at the bottom-left will provide you with a pre-filled YAML with your options. The player options page also has a link to download a full template file for that game which will have every option diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index 9f9eae5f..bb4c4959 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -51,8 +51,6 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder 1. Visit the [Player Options](../../../../games/Overcooked!%202/player-options) page and configure the game-specific options to taste -*By default, these options will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Options](../../../../weighted-options) page* - 2. Export your yaml file and use it to generate a new randomized game *For instructions on how to generate an Archipelago game, refer to the [Archipelago Setup Guide](../../../../tutorial/Archipelago/setup/en)* diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 391d5c29..49561097 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -29,8 +29,6 @@ There are three basic ways to get a yaml: Remember the name you enter in the options page or in the yaml file, you'll need it to connect later! -Note that the basic Player Options page doesn't allow you to change all advanced options, such as excluding particular units or upgrades. Go through the [Weighted Options](https://archipelago.gg/weighted-options) page for that. - Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information. ### Common yaml questions