From 093d66ba733ee00fb379622957156a8348e6bbcd Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 3 Sep 2023 00:15:17 -0500 Subject: [PATCH 01/14] WebHost: Developer-defined game option presets. --- WebHostLib/options.py | 11 +- WebHostLib/static/assets/player-settings.js | 112 ++++++++++++++++++- WebHostLib/static/styles/player-settings.css | 17 +++ WebHostLib/templates/player-settings.html | 23 +++- worlds/AutoWorld.py | 3 + worlds/rogue_legacy/Presets.py | 58 ++++++++++ worlds/rogue_legacy/__init__.py | 2 + 7 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 worlds/rogue_legacy/Presets.py diff --git a/WebHostLib/options.py b/WebHostLib/options.py index fca01407e06b..d1e0be31ccc1 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -3,11 +3,8 @@ import os import typing -import yaml -from jinja2 import Template - import Options -from Utils import __version__, local_path +from Utils import local_path from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", @@ -28,7 +25,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: weighted_settings = { "baseOptions": { "description": "Generated by https://archipelago.gg/", - "name": "Player", + "name": "", "game": {}, }, "games": {}, @@ -46,7 +43,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: "baseOptions": { "description": f"Generated by https://archipelago.gg/ for {game_name}", "game": game_name, - "name": "Player", + "name": "", }, } @@ -124,6 +121,8 @@ def get_html_doc(option_type: type(Options.Option)) -> str: player_settings["gameOptions"] = game_options + player_settings["presetOptions"] = world.web.settings_presets + os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True) with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index f75ba9060303..9427fc37d8ca 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -36,6 +36,17 @@ window.addEventListener('load', () => { const nameInput = document.getElementById('player-name'); nameInput.addEventListener('keyup', (event) => updateBaseSetting(event)); nameInput.value = playerSettings.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); @@ -45,7 +56,8 @@ window.addEventListener('load', () => { const resetSettings = () => { localStorage.removeItem(gameName); - localStorage.removeItem(`${gameName}-hash`) + localStorage.removeItem(`${gameName}-hash`); + localStorage.removeItem(`${gameName}-preset`); window.location.reload(); }; @@ -77,6 +89,10 @@ const createDefaultSettings = (settingData) => { } localStorage.setItem(gameName, JSON.stringify(newSettings)); } + + if (!localStorage.getItem(`${gameName}-preset`)) { + localStorage.setItem(`${gameName}-preset`, "__default"); + } }; const buildUI = (settingData) => { @@ -162,6 +178,7 @@ const buildOptionsTable = (settings, romOpts = false) => { element.classList.add('range-container'); let range = document.createElement('input'); + range.setAttribute('id', setting); range.setAttribute('type', 'range'); range.setAttribute('data-key', setting); range.setAttribute('min', settings[setting].min); @@ -294,6 +311,75 @@ const buildOptionsTable = (settings, romOpts = false) => { return table; }; +const setPresets = (settingsData, presetName) => { + const defaults = settingsData["gameOptions"]; + const preset = settingsData["presetOptions"][presetName]; + + localStorage.setItem(`${gameName}-preset`, presetName); + + if (!preset) { + console.error(`No presets defined for preset name: "${presetName}"`); + return; + } + + for (const setting in defaults) { + let presetValue = preset[setting]; + if (presetValue === undefined) { + // Using the default value if not set in presets. + presetValue = defaults[setting]["defaultValue"]; + } + + switch (defaults[setting].type) { + case "range": + case "select": { + const optionElement = document.querySelector(`#${setting}[data-key="${setting}"]`); + const randomElement = document.querySelector(`.randomize-button[data-key="${setting}"]`); + + if (presetValue === "random") { + randomElement.classList.add("active"); + optionElement.disabled = true; + updateGameSetting(randomElement, false); + } else { + optionElement.value = presetValue; + randomElement.classList.remove("active"); + optionElement.disabled = undefined; + updateGameSetting(optionElement, false); + } + + break; + } + + case "special_range": { + const selectElement = document.querySelector(`select[data-key="${setting}"]`); + const rangeElement = document.querySelector(`input[data-key="${setting}"]`); + const randomElement = document.querySelector(`.randomize-button[data-key="${setting}"]`); + + if (presetValue === "random") { + randomElement.classList.add("active"); + selectElement.disabled = true; + rangeElement.disabled = true; + updateGameSetting(randomElement, false); + } else { + rangeElement.value = presetValue; + selectElement.value = Object.values(defaults[setting]["value_names"]).includes(parseInt(presetValue)) ? + parseInt(presetValue) : "custom"; + document.getElementById(`${setting}-value`).innerText = presetValue; + + randomElement.classList.remove("active"); + selectElement.disabled = undefined; + rangeElement.disabled = undefined; + updateGameSetting(rangeElement, false); + } + break; + } + + default: + console.warn(`Ignoring preset value for unknown setting type: ${defaults[setting].type} with name ${setting}`); + break; + } + } +}; + const toggleRandomize = (event, inputElement, optionalSelectElement = null) => { const active = event.target.classList.contains('active'); const randomButton = event.target; @@ -322,9 +408,15 @@ const updateBaseSetting = (event) => { localStorage.setItem(gameName, JSON.stringify(options)); }; -const updateGameSetting = (settingElement) => { +const updateGameSetting = (settingElement, 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 (settingElement.classList.contains('randomize-button')) { // If the event passed in is the randomize button, then we know what we must do. options[gameName][settingElement.getAttribute('data-key')] = 'random'; @@ -338,7 +430,21 @@ const updateGameSetting = (settingElement) => { const exportSettings = () => { const settings = JSON.parse(localStorage.getItem(gameName)); - if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) { + const preset = localStorage.getItem(`${gameName}-preset`); + switch (preset) { + case "__default": + settings["description"] = `Generated by https://archipelago.gg with the default preset.`; + break; + + case "__custom": + settings["description"] = `Generated by https://archipelago.gg.`; + break; + + default: + settings["description"] = `Generated by https://archipelago.gg with the ${preset} preset.`; + } + + if (!settings.name || settings.name.trim().length === 0) { return showUserMessage('You must enter a player name!'); } const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); diff --git a/WebHostLib/static/styles/player-settings.css b/WebHostLib/static/styles/player-settings.css index e6e0c292922a..bc8252560bb5 100644 --- a/WebHostLib/static/styles/player-settings.css +++ b/WebHostLib/static/styles/player-settings.css @@ -90,6 +90,23 @@ html{ flex-direction: row; } +#player-settings #meta-options { + display: flex; + flex-direction: column; + gap: 6px; +} + +#player-settings #meta-options label { + display: inline-block; + min-width: 180px; +} + +#player-settings #meta-options input, +#player-settings #meta-options select { + box-sizing: border-box; + min-width: 200px; +} + #player-settings .left, #player-settings .right{ flex-grow: 1; } diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-settings.html index 50b9e3cbb1a2..f2ad8cf61b47 100644 --- a/WebHostLib/templates/player-settings.html +++ b/WebHostLib/templates/player-settings.html @@ -28,10 +28,25 @@

Player Settings

template file for this game.

-


- -

+

Meta Options

+
+
+ + +
+
+ + +
+ +

Game Options

diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 217269aa9927..fef8aacb4e3c 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -158,6 +158,9 @@ class WebWorld: bug_report_page: Optional[str] """display a link to a bug report page, most likely a link to a GitHub issue page.""" + settings_presets: Dict[str, Dict[str, Any]] = {} + """A dictionary containing a collection of developer-defined game option presets.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py new file mode 100644 index 000000000000..04a49c2e648c --- /dev/null +++ b/worlds/rogue_legacy/Presets.py @@ -0,0 +1,58 @@ +from typing import Any + +from .Options import * + +rl_option_presets: Dict[str, Dict[str, Any]] = { + "All Random": { + "progression_balancing": "random", + "accessibility": "random", + "starting_gender": "random", + "starting_class": "random", + "new_game_plus": "random", + "fairy_chests_per_zone": "random", + "chests_per_zone": "random", + "universal_fairy_chests": "random", + "universal_chests": "random", + "vendors": "random", + "architect": "random", + "architect_fee": "random", + "disable_charon": "random", + "require_purchasing": "random", + "progressive_blueprints": "random", + "gold_gain_multiplier": "random", + "number_of_children": "random", + "free_diary_on_generation": "random", + "khidr": "random", + "alexander": "random", + "leon": "random", + "herodotus": "random", + "health_pool": "random", + "mana_pool": "random", + "attack_pool": "random", + "magic_damage_pool": "random", + "armor_pool": "random", + "equip_pool": "random", + "crit_chance_pool": "random", + "crit_damage_pool": "random", + "allow_default_names": "random", + "death_link": "random", + }, + "Limited Resources": { + "progression_balancing": 0, + "fairy_chests_per_zone": 2, + "chests_per_zone": 30, + "vendors": "normal", + "architect": "disabled", + "gold_gain_multiplier": "half", + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 0, + "crit_damage_pool": 0, + } +} diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 68a0c856c8ad..2a0f1fca89f2 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -5,6 +5,7 @@ from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table from .Options import rl_options +from .Presets import rl_option_presets from .Regions import create_regions from .Rules import set_rules @@ -22,6 +23,7 @@ class RLWeb(WebWorld): )] bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \ "report-an-issue---.md&title=%5BIssue%5D" + settings_presets = rl_option_presets class RLWorld(World): From 3bfc90219fc29a4dd830f3237727a23973c6e273 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 3 Sep 2023 00:17:25 -0500 Subject: [PATCH 02/14] Tweak name --- WebHostLib/options.py | 2 +- worlds/AutoWorld.py | 2 +- worlds/rogue_legacy/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index d1e0be31ccc1..e9a2f3bbc2bc 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -121,7 +121,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: player_settings["gameOptions"] = game_options - player_settings["presetOptions"] = world.web.settings_presets + player_settings["presetOptions"] = world.web.options_presets os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index fef8aacb4e3c..31729c39fe43 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -158,7 +158,7 @@ class WebWorld: bug_report_page: Optional[str] """display a link to a bug report page, most likely a link to a GitHub issue page.""" - settings_presets: Dict[str, Dict[str, Any]] = {} + options_presets: Dict[str, Dict[str, Any]] = {} """A dictionary containing a collection of developer-defined game option presets.""" diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 2a0f1fca89f2..e4ad8cf4d233 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -23,7 +23,7 @@ class RLWeb(WebWorld): )] bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \ "report-an-issue---.md&title=%5BIssue%5D" - settings_presets = rl_option_presets + options_presets = rl_option_presets class RLWorld(World): From 657e415269bc6a01a63d71733036bf4a99375084 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 3 Sep 2023 00:29:51 -0500 Subject: [PATCH 03/14] Optimize imports --- worlds/rogue_legacy/Presets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py index 04a49c2e648c..82760a62b373 100644 --- a/worlds/rogue_legacy/Presets.py +++ b/worlds/rogue_legacy/Presets.py @@ -1,6 +1,4 @@ -from typing import Any - -from .Options import * +from typing import Any, Dict rl_option_presets: Dict[str, Dict[str, Any]] = { "All Random": { From 0393c4c62ed2696fe9d09b8d29e24ac3b6a7a3a0 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 3 Sep 2023 00:30:24 -0500 Subject: [PATCH 04/14] Fix possible gen issue --- worlds/rogue_legacy/Presets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py index 82760a62b373..9672fb4b59e6 100644 --- a/worlds/rogue_legacy/Presets.py +++ b/worlds/rogue_legacy/Presets.py @@ -32,7 +32,7 @@ "equip_pool": "random", "crit_chance_pool": "random", "crit_damage_pool": "random", - "allow_default_names": "random", + "allow_default_names": True, "death_link": "random", }, "Limited Resources": { From c1e6abd04b2342d3fd2bffcd399a5f3ba6938b14 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 12 Nov 2023 20:23:26 -0600 Subject: [PATCH 05/14] Layout improvements --- WebHostLib/options.py | 4 +- WebHostLib/static/assets/player-options.js | 73 +++++++++++---------- WebHostLib/static/styles/player-options.css | 19 +++++- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index a5245ab9a3eb..2ffe54f79538 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -118,7 +118,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: player_options["gameOptions"] = game_options - player_settings["presetOptions"] = world.web.options_presets + player_options["presetOptions"] = world.web.options_presets os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) @@ -136,7 +136,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: weighted_options["baseOptions"]["game"][game_name] = 0 weighted_options["games"][game_name] = { - "gameSettings": game_options, + "gameOptions": game_options, "gameItems": tuple(world.item_names), "gameItemGroups": [ group for group in world.item_name_groups.keys() if group != "Everything" diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index f5ea4705752b..4a95271f6075 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -16,8 +16,9 @@ window.addEventListener('load', () => { } 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."); + 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); } @@ -38,15 +39,15 @@ window.addEventListener('load', () => { 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"); + 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"] = {}; + results['presetOptions']['__default'] = {}; }).catch((e) => { console.error(e); const url = new URL(window.location.href); @@ -91,7 +92,7 @@ const createDefaultOptions = (optionData) => { } if (!localStorage.getItem(`${gameName}-preset`)) { - localStorage.setItem(`${gameName}-preset`, "__default"); + localStorage.setItem(`${gameName}-preset`, '__default'); } }; @@ -178,7 +179,7 @@ const buildOptionsTable = (options, romOpts = false) => { element.classList.add('range-container'); let range = document.createElement('input'); - range.setAttribute('id', setting); + range.setAttribute('id', option); range.setAttribute('type', 'range'); range.setAttribute('data-key', option); range.setAttribute('min', options[option].min); @@ -226,7 +227,7 @@ const buildOptionsTable = (options, romOpts = false) => { for (let i = 0; i < words.length; i++) { words[i] = words[i][0].toUpperCase() + words[i].substring(1); } - presetOption.innerText = words.join(" "); + presetOption.innerText = words.join(' '); specialRangeSelect.appendChild(presetOption); }); let customOption = document.createElement('option'); @@ -312,13 +313,13 @@ const buildOptionsTable = (options, romOpts = false) => { }; const setPresets = (optionsData, presetName) => { - const defaults = optionsData["gameOptions"]; - const preset = optionsData["presetOptions"][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}"`); + console.error(`No presets defined for preset name: '${presetName}'`); return; } @@ -326,22 +327,22 @@ const setPresets = (optionsData, presetName) => { let presetValue = preset[option]; if (presetValue === undefined) { // Using the default value if not set in presets. - presetValue = defaults[option]["defaultValue"]; + presetValue = defaults[option]['defaultValue']; } switch (defaults[option].type) { - case "range": - case "select": { - const optionElement = document.querySelector(`#${option}[data-key="${option}"]`); - const randomElement = document.querySelector(`.randomize-button[data-key="${option}"]`); + case 'range': + case 'select': { + const optionElement = document.querySelector(`#${option}[data-key='${option}']`); + const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); - if (presetValue === "random") { - randomElement.classList.add("active"); + if (presetValue === 'random') { + randomElement.classList.add('active'); optionElement.disabled = true; updateGameOption(randomElement, false); } else { optionElement.value = presetValue; - randomElement.classList.remove("active"); + randomElement.classList.remove('active'); optionElement.disabled = undefined; updateGameOption(optionElement, false); } @@ -349,23 +350,23 @@ const setPresets = (optionsData, presetName) => { break; } - case "special_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}"]`); + case 'special_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"); + 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"; + selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ? + parseInt(presetValue) : 'custom'; document.getElementById(`${option}-value`).innerText = presetValue; - randomElement.classList.remove("active"); + randomElement.classList.remove('active'); selectElement.disabled = undefined; rangeElement.disabled = undefined; updateGameOption(rangeElement, false); @@ -411,7 +412,7 @@ const updateGameOption = (optionElement, toggleCustomPreset = true) => { const options = JSON.parse(localStorage.getItem(gameName)); if (toggleCustomPreset) { - localStorage.setItem(`${gameName}-preset`, "__custom"); + localStorage.setItem(`${gameName}-preset`, '__custom'); const presetElement = document.getElementById('game-options-preset'); presetElement.value = '__custom'; } @@ -431,16 +432,16 @@ 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.`; + case '__default': + options['description'] = `Generated by https://archipelago.gg with the default preset.`; break; - case "__custom": - options["description"] = `Generated by https://archipelago.gg.`; + case '__custom': + options['description'] = `Generated by https://archipelago.gg.`; break; default: - options["description"] = `Generated by https://archipelago.gg with the ${preset} preset.`; + options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`; } if (!options.name || options.name.trim().length === 0) { diff --git a/WebHostLib/static/styles/player-options.css b/WebHostLib/static/styles/player-options.css index 0c113e8b2272..ae32bddc87a3 100644 --- a/WebHostLib/static/styles/player-options.css +++ b/WebHostLib/static/styles/player-options.css @@ -92,19 +92,26 @@ html{ #player-options #meta-options { display: flex; - flex-direction: column; - gap: 6px; + justify-content: space-between; + gap: 20px; +} + +#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: 200px; + min-width: 150px; + width: 50%; } #player-options .left, #player-options .right{ @@ -205,6 +212,12 @@ html{ 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; From 085d8b65d1348cae97947459342b5a48b7e8f68b Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 12 Nov 2023 20:54:54 -0600 Subject: [PATCH 06/14] Fix number in ranges not updating on reload/preset. --- WebHostLib/static/assets/player-options.js | 57 ++++++++++++++-------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 4a95271f6075..54fae2909a99 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -101,8 +101,11 @@ const buildUI = (optionData) => { 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]; } + 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)); @@ -137,7 +140,7 @@ const buildOptionsTable = (options, romOpts = false) => { const randomButton = document.createElement('button'); - switch(options[option].type){ + switch(options[option].type) { case 'select': element = document.createElement('div'); element.classList.add('select-container'); @@ -146,16 +149,17 @@ const buildOptionsTable = (options, romOpts = false) => { select.setAttribute('data-key', option); if (romOpts) { select.setAttribute('data-romOpt', '1'); } options[option].options.forEach((opt) => { - const option = document.createElement('option'); - option.setAttribute('value', opt.value); - option.innerText = opt.name; + 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])) { - option.selected = true; + optionElement.selected = true; } - select.appendChild(option); + select.appendChild(optionElement); }); select.addEventListener('change', (event) => updateGameOption(event.target)); element.appendChild(select); @@ -323,6 +327,22 @@ const setPresets = (optionsData, 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) { @@ -332,21 +352,20 @@ const setPresets = (optionsData, presetName) => { switch (defaults[option].type) { case 'range': - case 'select': { - const optionElement = document.querySelector(`#${option}[data-key='${option}']`); - const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); - + const numberElement = document.querySelector(`#${option}-value`); if (presetValue === 'random') { - randomElement.classList.add('active'); - optionElement.disabled = true; - updateGameOption(randomElement, false); + numberElement.innerText = defaults[option]['defaultValue'] === 'random' + ? defaults[option]['min'] // A fallback so we don't print 'random' in the UI. + : defaults[option]['defaultValue']; } else { - optionElement.value = presetValue; - randomElement.classList.remove('active'); - optionElement.disabled = undefined; - updateGameOption(optionElement, false); + numberElement.innerText = presetValue; } + updateOptionElement(option, presetValue); + break; + + case 'select': { + updateOptionElement(option, presetValue); break; } From bcee9a885a304f319cd3430aacb3507db444c4fc Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 12 Nov 2023 22:47:17 -0600 Subject: [PATCH 07/14] Add validation and clean up of some presets for RL. --- WebHostLib/options.py | 14 +++++++++++++- test/webhost/test_option_presets.py | 23 +++++++++++++++++++++++ worlds/rogue_legacy/Presets.py | 14 +++++++++----- 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 test/webhost/test_option_presets.py diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 2ffe54f79538..ba498202fe5d 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -118,7 +118,19 @@ def get_html_doc(option_type: type(Options.Option)) -> str: player_options["gameOptions"] = game_options - player_options["presetOptions"] = world.web.options_presets + player_options["presetOptions"] = {} + for preset_name, presets in world.web.options_presets.items(): + player_options["presetOptions"][preset_name] = {} + for option_name, option_value in presets.items(): + if option_value == "random": + player_options["presetOptions"][preset_name][option_name] = option_value + continue + + option = world.options_dataclass.type_hints[option_name].from_any(option_value) + if issubclass(option.__class__, Options.Range): + player_options["presetOptions"][preset_name][option_name] = option.value + else: + player_options["presetOptions"][preset_name][option_name] = option.current_key os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py new file mode 100644 index 000000000000..52180b515fb0 --- /dev/null +++ b/test/webhost/test_option_presets.py @@ -0,0 +1,23 @@ +import unittest +from typing import TYPE_CHECKING + +from worlds import AutoWorldRegister + +if TYPE_CHECKING: + from Options import Option + + +class TestOptionPresets(unittest.TestCase): + def test_option_presets_have_valid_options(self): + """Test that all predefined option presets are valid options.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + presets = world_type.web.options_presets + for preset_name, presets in presets.items(): + with self.subTest(game=game_name, presets=preset_name): + for option_name, option_value in presets.items(): + try: + # We don't need to assign the return, as we're only interested if this raises an error. + world_type.options_dataclass.type_hints[option_name].from_any(option_value) + except AssertionError as ex: + self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game " + f"'{game_name}' is not valid. Error: {ex}") diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py index 9672fb4b59e6..0bc22fc09891 100644 --- a/worlds/rogue_legacy/Presets.py +++ b/worlds/rogue_legacy/Presets.py @@ -1,7 +1,9 @@ from typing import Any, Dict + rl_option_presets: Dict[str, Dict[str, Any]] = { - "All Random": { + # Example preset. + "Unknown Fate": { "progression_balancing": "random", "accessibility": "random", "starting_gender": "random", @@ -32,12 +34,14 @@ "equip_pool": "random", "crit_chance_pool": "random", "crit_damage_pool": "random", - "allow_default_names": True, + "allow_default_names": 1, "death_link": "random", }, - "Limited Resources": { + # A preset I actually use. + "Limited Potential": { "progression_balancing": 0, "fairy_chests_per_zone": 2, + "starting_class": "random", "chests_per_zone": 30, "vendors": "normal", "architect": "disabled", @@ -50,7 +54,7 @@ "magic_damage_pool": 10, "armor_pool": 5, "equip_pool": 10, - "crit_chance_pool": 0, - "crit_damage_pool": 0, + "crit_chance_pool": 5, + "crit_damage_pool": 5, } } From 23cf8de63e797a0bccc3e8cc590902c5f6632c66 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 12 Nov 2023 22:58:50 -0600 Subject: [PATCH 08/14] Add documentation. --- docs/world api.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/world api.md b/docs/world api.md index 9b7573dccd9d..635c73e1cc20 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -73,6 +73,51 @@ for your world specifically on the webhost: `game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be prefixed with the same string as defined here. Default already has 'en'. +`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values +are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of +the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. + +Note: The values must be valid for the option type and can only include the following option types: + + - If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. + - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. + - `random` is also a valid value for any of these option types. + +`OptionDict`, `OptionList`, `OptionSet`, and `FreeText` are not supported. + +Here is an example of a defined preset: +```python +# Presets.py +options_presets = { + "Limited Potential": { + "progression_balancing": 0, + "fairy_chests_per_zone": 2, + "starting_class": "random", + "chests_per_zone": 30, + "vendors": "normal", + "architect": "disabled", + "gold_gain_multiplier": "half", + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 5, + "crit_damage_pool": 5, + } +} + +# __init__.py +class RLWeb(WebWorld): + options_presets = options_presets + # ... +``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations From a34a27be7b384266175d4b4f68af9653c9f08f73 Mon Sep 17 00:00:00 2001 From: Phar Date: Sun, 12 Nov 2023 23:20:22 -0600 Subject: [PATCH 09/14] Revert accidental change. --- WebHostLib/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index ba498202fe5d..fb1a637111d1 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -148,7 +148,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str: weighted_options["baseOptions"]["game"][game_name] = 0 weighted_options["games"][game_name] = { - "gameOptions": game_options, + "gameSettings": game_options, "gameItems": tuple(world.item_names), "gameItemGroups": [ group for group in world.item_name_groups.keys() if group != "Everything" From d2fbd89a47fb7a616b709f6fa8ea2d570ed89dcf Mon Sep 17 00:00:00 2001 From: Phar Date: Mon, 13 Nov 2023 09:12:42 -0600 Subject: [PATCH 10/14] Additional edge cases, variable name changes, and some tweaks to UI. --- WebHostLib/options.py | 29 ++++++++++-- WebHostLib/templates/player-options.html | 3 +- docs/world api.md | 8 ++-- test/webhost/test_option_presets.py | 59 ++++++++++++++++++++---- worlds/rogue_legacy/Presets.py | 17 +++---- worlds/rogue_legacy/__init__.py | 4 +- 6 files changed, 93 insertions(+), 27 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index fb1a637111d1..49146c9ddb34 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -119,17 +119,40 @@ def get_html_doc(option_type: type(Options.Option)) -> str: player_options["gameOptions"] = game_options player_options["presetOptions"] = {} - for preset_name, presets in world.web.options_presets.items(): + for preset_name, preset in world.web.options_presets.items(): player_options["presetOptions"][preset_name] = {} - for option_name, option_value in presets.items(): + for option_name, option_value in preset.items(): + # Random range type settings are not valid. + assert (option_value not in ["random-low", "random-high", "random-middle"] and + 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 continue option = world.options_dataclass.type_hints[option_name].from_any(option_value) - if issubclass(option.__class__, Options.Range): + if isinstance(option, Options.SpecialRange) 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}." + + # 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 os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) diff --git a/WebHostLib/templates/player-options.html b/WebHostLib/templates/player-options.html index 7c35241ecbb4..4c749752882a 100644 --- a/WebHostLib/templates/player-options.html +++ b/WebHostLib/templates/player-options.html @@ -28,7 +28,6 @@

Player Options

template file for this game.

-

Meta Options